2020-10-19 13:23:42 +00:00
#! /bin/bash
2019-08-08 19:33:55 +00:00
load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'
2021-01-18 19:51:56 +00:00
NAME = ${ NAME :- mailserver -testing : ci }
2019-08-08 19:33:55 +00:00
2019-09-05 19:35:23 +00:00
# default timeout is 120 seconds
TEST_TIMEOUT_IN_SECONDS = ${ TEST_TIMEOUT_IN_SECONDS -120 }
2019-09-11 22:38:34 +00:00
NUMBER_OF_LOG_LINES = ${ NUMBER_OF_LOG_LINES -10 }
2019-08-08 19:33:55 +00:00
2020-10-18 00:08:11 +00:00
# @param ${1} timeout
2020-09-17 21:40:39 +00:00
# @param --fatal-test <command eval string> additional test whose failure aborts immediately
# @param ... test to run
2019-08-08 19:33:55 +00:00
function repeat_until_success_or_timeout {
2020-10-18 13:44:01 +00:00
local FATAL_FAILURE_TEST_COMMAND
2020-10-18 00:08:11 +00:00
if [ [ " ${ 1 } " = = "--fatal-test" ] ] ; then
2020-10-18 13:44:01 +00:00
FATAL_FAILURE_TEST_COMMAND = " ${ 2 } "
2020-09-18 00:21:24 +00:00
shift 2
fi
2020-10-18 00:08:11 +00:00
if ! [ [ " ${ 1 } " = ~ ^[ 0-9] +$ ] ] ; then
echo " First parameter for timeout must be an integer, recieved \" ${ 1 } \" "
2019-09-10 22:02:16 +00:00
return 1
2019-08-08 19:33:55 +00:00
fi
2020-10-18 00:08:11 +00:00
local TIMEOUT = ${ 1 }
local STARTTIME = ${ SECONDS }
2019-08-08 19:33:55 +00:00
shift 1
2020-10-18 00:08:11 +00:00
until " ${ @ } "
2019-08-08 19:33:55 +00:00
do
2020-10-19 12:10:32 +00:00
if [ [ -n ${ FATAL_FAILURE_TEST_COMMAND } ] ] && ! eval " ${ FATAL_FAILURE_TEST_COMMAND } " ; then
2020-10-18 13:44:01 +00:00
echo " \` ${ FATAL_FAILURE_TEST_COMMAND } \` failed, early aborting repeat_until_success of \` ${ * } \` " >& 2
2020-10-18 01:11:10 +00:00
return 1
2020-09-17 21:40:39 +00:00
fi
2020-10-18 01:11:10 +00:00
sleep 1
2020-10-18 00:08:11 +00:00
if [ [ $(( SECONDS - STARTTIME )) -gt ${ TIMEOUT } ] ] ; then
echo " Timed out on command: ${ * } " >& 2
2019-09-10 22:02:16 +00:00
return 1
2019-08-08 19:33:55 +00:00
fi
done
}
2020-09-19 23:10:05 +00:00
# like repeat_until_success_or_timeout but with wrapping the command to run into `run` for later bats consumption
2020-10-18 00:08:11 +00:00
# @param ${1} timeout
2020-09-19 23:10:05 +00:00
# @param ... test command to run
function run_until_success_or_timeout {
2020-10-19 12:10:32 +00:00
if ! [ [ ${ 1 } = ~ ^[ 0-9] +$ ] ] ; then
2020-10-18 00:08:11 +00:00
echo " First parameter for timeout must be an integer, recieved \" ${ 1 } \" "
2020-09-19 23:10:05 +00:00
return 1
fi
2020-10-18 00:08:11 +00:00
local TIMEOUT = ${ 1 }
local STARTTIME = ${ SECONDS }
2020-09-19 23:10:05 +00:00
shift 1
2020-10-19 12:10:32 +00:00
until run " ${ @ } " && [ [ $status -eq 0 ] ]
2020-09-19 23:10:05 +00:00
do
sleep 1
2020-10-18 01:11:10 +00:00
if ( ( SECONDS - STARTTIME > TIMEOUT ) ) ; then
2020-10-18 00:08:11 +00:00
echo " Timed out on command: ${ * } " >& 2
2020-09-19 23:10:05 +00:00
return 1
fi
done
}
2020-10-18 00:08:11 +00:00
# @param ${1} timeout
# @param ${2} container name
2020-09-17 22:02:38 +00:00
# @param ... test command for container
function repeat_in_container_until_success_or_timeout( ) {
2020-10-18 00:08:11 +00:00
local TIMEOUT = " ${ 1 } "
local CONTAINER_NAME = " ${ 2 } "
2020-09-17 22:02:38 +00:00
shift 2
2020-10-18 00:08:11 +00:00
repeat_until_success_or_timeout --fatal-test " container_is_running ${ CONTAINER_NAME } " " ${ TIMEOUT } " docker exec " ${ CONTAINER_NAME } " " ${ @ } "
2020-09-17 22:02:38 +00:00
}
2020-09-17 21:40:39 +00:00
function container_is_running( ) {
2020-10-18 00:08:11 +00:00
[ [ " $( docker inspect -f '{{.State.Running}}' " ${ 1 } " ) " = = "true" ] ]
2020-09-17 21:40:39 +00:00
}
2020-10-18 00:08:11 +00:00
# @param ${1} port
# @param ${2} container name
2019-10-08 21:11:27 +00:00
function wait_for_tcp_port_in_container( ) {
2020-10-19 12:10:32 +00:00
repeat_until_success_or_timeout --fatal-test " container_is_running ${ 2 } " " ${ TEST_TIMEOUT_IN_SECONDS } " docker exec " ${ 2 } " /bin/sh -c " nc -z 0.0.0.0 ${ 1 } "
2019-10-08 21:11:27 +00:00
}
2020-10-18 00:08:11 +00:00
# @param ${1} name of the postfix container
2019-08-08 19:33:55 +00:00
function wait_for_smtp_port_in_container( ) {
2020-10-18 00:08:11 +00:00
wait_for_tcp_port_in_container 25 " ${ 1 } "
2019-10-08 21:11:27 +00:00
}
2021-09-06 10:13:12 +00:00
# @param ${1} name of the postfix container
function wait_for_smtp_port_in_container_to_respond( ) {
local COUNT = 0
until [ [ $( docker exec " ${ 1 } " timeout 10 /bin/sh -c "echo QUIT | nc localhost 25" ) = = *"221 2.0.0 Bye" * ] ] ; do
if [ [ $COUNT -eq 20 ] ]
then
echo "Unable to receive a valid response from 'nc localhost 25' within 20 seconds"
return 1
fi
sleep 1
( ( COUNT += 1) )
done
}
2020-10-18 00:08:11 +00:00
# @param ${1} name of the postfix container
2019-10-08 21:11:27 +00:00
function wait_for_amavis_port_in_container( ) {
2020-10-18 00:08:11 +00:00
wait_for_tcp_port_in_container 10024 " ${ 1 } "
2019-08-08 19:33:55 +00:00
}
2021-09-20 07:35:03 +00:00
# TODO: Should also fail early on "docker logs ${1} | egrep '^[ FATAL ]'"?
2020-10-18 00:08:11 +00:00
# @param ${1} name of the postfix container
2019-08-08 19:33:55 +00:00
function wait_for_finished_setup_in_container( ) {
2020-10-18 13:44:01 +00:00
local STATUS = 0
repeat_until_success_or_timeout --fatal-test " container_is_running ${ 1 } " " ${ TEST_TIMEOUT_IN_SECONDS } " sh -c " docker logs ${ 1 } | grep 'is up and running' " || STATUS = 1
if [ [ ${ STATUS } -eq 1 ] ] ; then
2020-10-18 00:08:11 +00:00
echo " Last ${ NUMBER_OF_LOG_LINES } lines of container \` ${ 1 } \`'s log "
docker logs " ${ 1 } " | tail -n " ${ NUMBER_OF_LOG_LINES } "
2019-09-11 22:38:34 +00:00
fi
2020-10-18 13:44:01 +00:00
return ${ STATUS }
2019-08-08 23:34:04 +00:00
}
2020-10-18 00:08:11 +00:00
SETUP_FILE_MARKER = " ${ BATS_TMPDIR } / $( basename " ${ BATS_TEST_FILENAME } " ) .setup_file "
2019-09-10 23:01:51 +00:00
2020-09-19 23:09:39 +00:00
function native_setup_teardown_file_support( ) {
2020-10-18 00:08:11 +00:00
local VERSION_REGEX = '([0-9]+)\.([0-9]+)\.([0-9]+)'
2020-09-19 23:09:39 +00:00
# bats versions that support setup_file out of the box don't need this
2020-10-18 00:08:11 +00:00
if [ [ " ${ BATS_VERSION } " = ~ ${ VERSION_REGEX } ] ] ; then
2020-09-19 23:09:39 +00:00
numeric_version = $(( ( BASH_REMATCH[ 1 ] * 100 + BASH_REMATCH[ 2 ] ) * 100 + BASH_REMATCH[ 3 ] ))
2020-10-18 00:08:11 +00:00
if [ [ ${ numeric_version } -ge 10201 ] ] ; then
if [ " ${ BATS_TEST_NAME } " = = 'test_first' ] ; then
2020-09-19 23:09:39 +00:00
skip 'This version natively supports setup/teardown_file'
fi
return 0
fi
fi
return 1
}
2019-08-08 23:34:04 +00:00
# use in setup() in conjunction with a `@test "first" {}` to trigger setup_file reliably
function run_setup_file_if_necessary( ) {
2020-09-19 23:09:39 +00:00
native_setup_teardown_file_support && return 0
2020-10-18 00:08:11 +00:00
if [ " ${ BATS_TEST_NAME } " = = 'test_first' ] ; then
2019-09-11 22:37:34 +00:00
# prevent old markers from marking success or get an error if we cannot remove due to permissions
2020-10-18 00:08:11 +00:00
rm -f " ${ SETUP_FILE_MARKER } "
2019-09-11 22:37:34 +00:00
2019-08-08 23:34:04 +00:00
setup_file
2019-09-11 22:37:34 +00:00
2020-10-18 00:08:11 +00:00
touch " ${ SETUP_FILE_MARKER } "
2019-09-10 23:01:51 +00:00
else
2020-10-18 00:08:11 +00:00
if [ ! -f " ${ SETUP_FILE_MARKER } " ] ; then
2019-09-10 23:01:51 +00:00
skip "setup_file failed"
return 1
fi
2019-08-08 23:34:04 +00:00
fi
}
# use in teardown() in conjunction with a `@test "last" {}` to trigger teardown_file reliably
function run_teardown_file_if_necessary( ) {
2020-09-19 23:09:39 +00:00
native_setup_teardown_file_support && return 0
2020-10-18 00:08:11 +00:00
if [ " ${ BATS_TEST_NAME } " = = 'test_last' ] ; then
2019-09-11 22:37:34 +00:00
# cleanup setup file marker
2020-10-18 00:08:11 +00:00
rm -f " ${ SETUP_FILE_MARKER } "
2019-08-08 23:34:04 +00:00
teardown_file
fi
2019-09-05 19:35:23 +00:00
}
2020-09-17 23:37:42 +00:00
# get the private config path for the given container or test file, if no container name was given
function private_config_path( ) {
2020-10-18 00:08:11 +00:00
echo " ${ PWD } /test/duplicate_configs/ ${ 1 :- $( basename " ${ BATS_TEST_FILENAME } " ) } "
2020-09-17 23:37:42 +00:00
}
2020-10-18 00:08:11 +00:00
# @param ${1} relative source in test/config folder
# @param ${2} (optional) container name, defaults to ${BATS_TEST_FILENAME}
2020-09-17 23:37:42 +00:00
# @return path to the folder where the config is duplicated
function duplicate_config_for_container( ) {
2020-10-18 01:11:10 +00:00
local OUTPUT_FOLDER
2020-10-19 22:50:19 +00:00
OUTPUT_FOLDER = " $( private_config_path " ${ 2 } " ) " || return $?
rm -rf " ${ OUTPUT_FOLDER : ? } / " || return $? # cleanup
mkdir -p " ${ OUTPUT_FOLDER } " || return $?
cp -r " ${ PWD } /test/config/ ${ 1 : ? } /. " " ${ OUTPUT_FOLDER } " || return $?
2020-10-18 00:08:11 +00:00
echo " ${ OUTPUT_FOLDER } "
2020-10-01 12:57:05 +00:00
}
function container_has_service_running( ) {
2020-10-18 00:08:11 +00:00
local CONTAINER_NAME = " ${ 1 } "
local SERVICE_NAME = " ${ 2 } "
docker exec " ${ CONTAINER_NAME } " /usr/bin/supervisorctl status " ${ SERVICE_NAME } " | grep RUNNING >/dev/null
2020-10-01 12:57:05 +00:00
}
function wait_for_service( ) {
2020-10-18 00:08:11 +00:00
local CONTAINER_NAME = " ${ 1 } "
local SERVICE_NAME = " ${ 2 } "
2020-10-19 22:50:19 +00:00
repeat_until_success_or_timeout --fatal-test " container_is_running ${ CONTAINER_NAME } " " ${ TEST_TIMEOUT_IN_SECONDS } " \
2020-10-18 00:08:11 +00:00
container_has_service_running " ${ CONTAINER_NAME } " " ${ SERVICE_NAME } "
2020-10-01 12:57:05 +00:00
}
2020-10-01 23:20:31 +00:00
function wait_for_changes_to_be_detected_in_container( ) {
2020-10-18 00:08:11 +00:00
local CONTAINER_NAME = " ${ 1 } "
local TIMEOUT = ${ TEST_TIMEOUT_IN_SECONDS }
2021-01-16 09:16:05 +00:00
2020-10-19 12:10:32 +00:00
# shellcheck disable=SC2016
2020-10-28 13:00:31 +00:00
repeat_in_container_until_success_or_timeout " ${ TIMEOUT } " " ${ CONTAINER_NAME } " bash -c 'source /usr/local/bin/helper-functions.sh; cmp --silent -- <(_monitored_files_checksums) "${CHKSUM_FILE}" >/dev/null'
2020-10-17 21:24:23 +00:00
}
2020-10-19 21:33:54 +00:00
function wait_for_empty_mail_queue_in_container( ) {
local CONTAINER_NAME = " ${ 1 } "
local TIMEOUT = ${ TEST_TIMEOUT_IN_SECONDS }
2021-01-16 09:16:05 +00:00
# shellcheck disable=SC2016
2020-10-19 21:33:54 +00:00
repeat_in_container_until_success_or_timeout " ${ TIMEOUT } " " ${ CONTAINER_NAME } " bash -c '[[ $(mailq) == *"Mail queue is empty"* ]]'
}
2021-09-20 07:35:03 +00:00
# Common defaults appropriate for most tests, override vars in each test when necessary.
2021-11-04 20:35:01 +00:00
# For all tests override in `setup_file()` via an `export` var.
# For individual test override the var via `local` var instead.
#
# For example, if you need an immutable config volume that can't be affected by other tests
# in the file, then use `local TEST_TMP_CONFIG="$(duplicate_config_for_container . "${UNIQUE_ID_HERE}")"`
2021-09-20 07:35:03 +00:00
function init_with_defaults( ) {
2021-11-04 20:35:01 +00:00
export TEST_NAME TEST_TMP_CONFIG
# In `setup_file()` the default name to use for the currently tested docker container
# is `${TEST_NAME}` global defined here. It derives the name from the test filename:
# `basename` to ignore absolute dir path and file extension, only extract filename.
2021-09-20 07:35:03 +00:00
TEST_NAME = " $( basename " ${ BATS_TEST_FILENAME } " '.bats' ) "
2021-11-04 20:35:01 +00:00
# In `setup_file()` creates a single copy of the test config folder to use for an entire test file:
TEST_TMP_CONFIG = " $( duplicate_config_for_container . " ${ TEST_NAME } " ) "
# Common complimentary test files, read-only safe to share across containers:
export TEST_FILES_CONTAINER_PATH = '/tmp/docker-mailserver-test'
export TEST_FILES_VOLUME = " ${ PWD } /test/test-files: ${ TEST_FILES_CONTAINER_PATH } :ro "
2021-09-20 07:35:03 +00:00
2021-11-04 20:35:01 +00:00
# The config volume cannot be read-only as some data needs to be written at container startup
# - two sed failures (unknown lines)
# - dovecot-quotas.cf (setup-stack.sh:_setup_dovecot_quotas)
# - postfix-aliases.cf (setup-stack.sh:_setup_postfix_aliases)
# TODO: Check how many tests need write access. Consider using `docker create` + `docker cp` for easier cleanup.
export TEST_CONFIG_VOLUME = " ${ TEST_TMP_CONFIG } :/tmp/docker-mailserver "
2021-09-20 07:35:03 +00:00
2021-11-04 20:35:01 +00:00
# The common default FQDN assigned to the container `--hostname` option:
2021-09-20 07:35:03 +00:00
export TEST_FQDN = 'mail.my-domain.com'
2021-11-04 20:35:01 +00:00
# Default Root CA cert used in TLS tests with `openssl` commands:
export TEST_CA_CERT = " ${ TEST_FILES_CONTAINER_PATH } /ssl/example.test/with_ca/ecdsa/ca-cert.ecdsa.pem "
2021-09-20 07:35:03 +00:00
}
2021-11-04 20:35:01 +00:00
# Using `create` and `start` instead of only `run` allows to modify
# the container prior to starting it. Otherwise use this combined method.
# NOTE: Forwards all args to the create method at present.
2021-09-20 07:35:03 +00:00
function common_container_setup( ) {
2021-11-04 20:35:01 +00:00
common_container_create " $@ "
common_container_start
}
2021-09-20 07:35:03 +00:00
2021-11-04 20:35:01 +00:00
# Common docker setup is centralized here.
#
# `X_EXTRA_ARGS` - Optional: Pass an array by it's variable name as a string, it will
# be used as a reference for appending extra config into the `docker create` below:
#
# NOTE: Using array reference for a single input parameter, as this method is still
# under development while adapting tests to it and requirements it must serve (eg: support base config matrix in CI)
function common_container_create( ) {
local -n X_EXTRA_ARGS = ${ 1 }
run docker create --name " ${ TEST_NAME } " \
2021-09-20 07:35:03 +00:00
--hostname " ${ TEST_FQDN } " \
--tty \
2021-11-04 20:35:01 +00:00
--volume " ${ TEST_FILES_VOLUME } " \
--volume " ${ TEST_CONFIG_VOLUME } " \
" ${ X_EXTRA_ARGS [@] } " \
2021-09-20 07:35:03 +00:00
" ${ NAME } "
assert_success
2021-11-04 20:35:01 +00:00
}
function common_container_start( ) {
run docker start " ${ TEST_NAME } "
assert_success
2021-09-20 07:35:03 +00:00
wait_for_finished_setup_in_container " ${ TEST_NAME } "
2021-11-04 20:35:01 +00:00
}