You've already forked docker-mailserver
mirror of
https://github.com/docker-mailserver/docker-mailserver.git
synced 2025-08-07 23:03:10 +02:00
tests(refactor): Improve consistency and documentation for test helpers (#3012)
This commit is contained in:
@ -1,29 +1,86 @@
|
||||
#!/bin/bash
|
||||
|
||||
# TODO: Functions need documentation (adhere to doc conventions!)
|
||||
# ? ABOUT: Functions defined here aid with the change-detection functionality of DMS.
|
||||
|
||||
# ! -------------------------------------------------------------------
|
||||
# ? >> Miscellaneous initialization functionality
|
||||
|
||||
# shellcheck disable=SC2155
|
||||
|
||||
load "${REPOSITORY_ROOT}/test/helper/common"
|
||||
|
||||
function wait_until_change_detection_event_begins() {
|
||||
# ? << Miscellaneous initialization functionality
|
||||
# ! -------------------------------------------------------------------
|
||||
# ? >> Change-detection helpers
|
||||
|
||||
# TODO documentation @polarathene
|
||||
#
|
||||
# ## Note
|
||||
#
|
||||
# Relies on ENV `LOG_LEVEL=debug` or higher
|
||||
#
|
||||
# @param ${1} = expected count [OPTIONAL]
|
||||
# @param ${2} = container name [OPTIONAL]
|
||||
function _wait_until_expected_count_is_matched() {
|
||||
function __get_count() {
|
||||
# NOTE: `|| true` required due to `set -e` usage:
|
||||
# https://github.com/docker-mailserver/docker-mailserver/pull/2997#discussion_r1070583876
|
||||
_exec_in_container grep --count "${MATCH_CONTENT}" "${MATCH_IN_LOG}" || true
|
||||
}
|
||||
|
||||
# WARNING: Keep in mind it is a '>=' comparison.
|
||||
# If you provide an explict count to match, ensure it is not too low to cause a false-positive.
|
||||
function __has_expected_count() {
|
||||
# shellcheck disable=SC2317
|
||||
[[ $(__get_count) -ge "${EXPECTED_COUNT}" ]]
|
||||
}
|
||||
|
||||
local EXPECTED_COUNT=${1:-}
|
||||
local CONTAINER_NAME=$(__handle_container_name "${2:-}")
|
||||
|
||||
# Ensure the container is configured with the required `LOG_LEVEL` ENV:
|
||||
assert_regex "$(_exec_in_container env | grep '^LOG_LEVEL=')" '=(debug|trace)$'
|
||||
|
||||
# Default behaviour is to wait until one new match is found (eg: incremented),
|
||||
# unless explicitly set (useful for waiting on a min count to be reached):
|
||||
#
|
||||
# +1 of starting count if EXPECTED_COUNT is empty
|
||||
[[ -n ${EXPECTED_COUNT} ]] || EXPECTED_COUNT=$(( $(__get_count) + 1 ))
|
||||
|
||||
_repeat_until_success_or_timeout 20 __has_expected_count
|
||||
}
|
||||
|
||||
function _wait_until_change_detection_event_begins() {
|
||||
local MATCH_CONTENT='Change detected'
|
||||
local MATCH_IN_LOG='/var/log/supervisor/changedetector.log'
|
||||
|
||||
_wait_until_expected_count_is_matched "${@}"
|
||||
}
|
||||
|
||||
# NOTE: Change events can start and finish all within < 1 sec,
|
||||
# Reliably track the completion of a change event by counting events:
|
||||
function wait_until_change_detection_event_completes() {
|
||||
# ## Note
|
||||
#
|
||||
# Change events can start and finish all within < 1 sec.
|
||||
# Reliably track the completion of a change event by counting events.
|
||||
function _wait_until_change_detection_event_completes() {
|
||||
# shellcheck disable=SC2034
|
||||
local MATCH_CONTENT='Completed handling of detected change'
|
||||
# shellcheck disable=SC2034
|
||||
local MATCH_IN_LOG='/var/log/supervisor/changedetector.log'
|
||||
|
||||
|
||||
_wait_until_expected_count_is_matched "${@}"
|
||||
}
|
||||
|
||||
function _get_logs_since_last_change_detection() {
|
||||
local CONTAINER_NAME=${1}
|
||||
# shellcheck disable=SC2034
|
||||
local CONTAINER_NAME=$(__handle_container_name "${1:-}")
|
||||
local MATCH_IN_FILE='/var/log/supervisor/changedetector.log'
|
||||
local MATCH_STRING='Change detected'
|
||||
|
||||
# Read file in reverse, collect lines until match with sed is found,
|
||||
# then stop and return these lines back in original order (flipped again through tac):
|
||||
docker exec "${CONTAINER_NAME}" bash -c "tac ${MATCH_IN_FILE} | sed '/${MATCH_STRING}/q' | tac"
|
||||
_exec_in_container_bash "tac ${MATCH_IN_FILE} | sed '/${MATCH_STRING}/q' | tac"
|
||||
}
|
||||
|
||||
# ? << Change-detection helpers
|
||||
# ! -------------------------------------------------------------------
|
||||
|
@ -1,5 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ? ABOUT: Functions defined here aid with common functionality during tests.
|
||||
|
||||
# ! ATTENTION: Functions prefixed with `__` are intended for internal use within this file only, not in tests.
|
||||
|
||||
# ! -------------------------------------------------------------------
|
||||
# ? >> Miscellaneous initialization functionality
|
||||
|
||||
# shellcheck disable=SC2155
|
||||
|
||||
# Load additional BATS libraries for more functionality.
|
||||
#
|
||||
# ## Note
|
||||
#
|
||||
# This function is internal and should not be used in tests.
|
||||
function __load_bats_helper() {
|
||||
load "${REPOSITORY_ROOT}/test/test_helper/bats-support/load"
|
||||
load "${REPOSITORY_ROOT}/test/test_helper/bats-assert/load"
|
||||
@ -7,241 +21,377 @@ function __load_bats_helper() {
|
||||
|
||||
__load_bats_helper
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
# like _run_in_container_explicit but infers ${1} by using the ENV CONTAINER_NAME
|
||||
# WARNING: Careful using this with _until_success_or_timeout methods,
|
||||
# which can be misleading in the success of `run`, not the command given to `run`.
|
||||
function _run_in_container() {
|
||||
run docker exec "${CONTAINER_NAME}" "${@}"
|
||||
# Properly handle the container name given to tests. This makes the whole
|
||||
# test suite more robust as we can be sure that the container name is
|
||||
# properly set. Sometimes, we need to provide an explicit container name;
|
||||
# this function eases the pain by either providing the explicitly given
|
||||
# name or `CONTAINER_NAME` if it is set.
|
||||
#
|
||||
# @param ${1} = explicit container name [OPTIONAL]
|
||||
#
|
||||
# ## Attention
|
||||
#
|
||||
# Note that this function checks whether the name given to it starts with
|
||||
# the prefix `dms-test_`. One must adhere to this naming convention.
|
||||
#
|
||||
# ## Panics
|
||||
#
|
||||
# If neither an explicit non-empty argument is given nor `CONTAINER_NAME`
|
||||
# is set.
|
||||
#
|
||||
# ## "Calling Convention"
|
||||
#
|
||||
# This function should be called the following way:
|
||||
#
|
||||
# local SOME_VAR=$(__handle_container_name "${X:-}")
|
||||
#
|
||||
# Where `X` is an arbitrary argument of the function you're calling.
|
||||
#
|
||||
# ## Note
|
||||
#
|
||||
# This function is internal and should not be used in tests.
|
||||
function __handle_container_name() {
|
||||
if [[ -n ${1:-} ]] && [[ ${1:-} =~ ^dms-test_ ]]
|
||||
then
|
||||
printf '%s' "${1}"
|
||||
return 0
|
||||
elif [[ -n ${CONTAINER_NAME+set} ]]
|
||||
then
|
||||
printf '%s' "${CONTAINER_NAME}"
|
||||
return 0
|
||||
else
|
||||
echo 'ERROR: (helper/common.sh) Container name was either provided explicitly without the required `dms-test_` prefix, or CONTAINER_NAME is not set for implicit usage' >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# @param ${1} container name [REQUIRED]
|
||||
# @param ... command to execute
|
||||
function _run_in_container_explicit() {
|
||||
local CONTAINER_NAME=${1:?Container name must be given when using explicit}
|
||||
# ? << Miscellaneous initialization functionality
|
||||
# ! -------------------------------------------------------------------
|
||||
# ? >> Functions to execute commands inside a container
|
||||
|
||||
|
||||
# Execute a command inside a container with an explicit name.
|
||||
#
|
||||
# @param ${1} = container name
|
||||
# @param ... = command to execute
|
||||
function _exec_in_container_explicit() {
|
||||
local CONTAINER_NAME=${1:?Container name must be provided when using explicit}
|
||||
shift 1
|
||||
run docker exec "${CONTAINER_NAME}" "${@}"
|
||||
docker exec "${CONTAINER_NAME}" "${@}"
|
||||
}
|
||||
|
||||
function _default_teardown() {
|
||||
docker rm -f "${CONTAINER_NAME}"
|
||||
# Execute a command inside the container with name ${CONTAINER_NAME}.
|
||||
#
|
||||
# @param ... = command to execute
|
||||
function _exec_in_container() {
|
||||
_exec_in_container_explicit "${CONTAINER_NAME:?Container name must be provided}" "${@}"
|
||||
}
|
||||
|
||||
function _reload_postfix() {
|
||||
local CONTAINER_NAME=${1:-${CONTAINER_NAME}}
|
||||
|
||||
# Reloading Postfix config after modifying it in <2 sec will cause Postfix to delay, workaround that:
|
||||
docker exec "${CONTAINER_NAME}" touch -d '2 seconds ago' /etc/postfix/main.cf
|
||||
docker exec "${CONTAINER_NAME}" postfix reload
|
||||
# Execute a command inside a container with an explicit name. The command is run with
|
||||
# BATS' `run` so you can check the exit code and use `assert_`.
|
||||
#
|
||||
# @param ${1} = container name
|
||||
# @param ... = command to execute
|
||||
function _run_in_container_explicit() {
|
||||
local CONTAINER_NAME=${1:?Container name must be provided when using explicit}
|
||||
shift 1
|
||||
run _exec_in_container_explicit "${CONTAINER_NAME}" "${@}"
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
# @param ${1} target container name [IF UNSET: ${CONTAINER_NAME}]
|
||||
function get_container_ip() {
|
||||
local TARGET_CONTAINER_NAME=${1:-${CONTAINER_NAME}}
|
||||
docker inspect --format '{{ .NetworkSettings.IPAddress }}' "${TARGET_CONTAINER_NAME}"
|
||||
# Execute a command inside the container with name ${CONTAINER_NAME}. The command
|
||||
# is run with BATS' `run` so you can check the exit code and use `assert_`.
|
||||
#
|
||||
# @param ... = command to execute
|
||||
function _run_in_container() {
|
||||
_run_in_container_explicit "${CONTAINER_NAME:?Container name must be provided}" "${@}"
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Execute a command inside the container with name ${CONTAINER_NAME}. Moreover,
|
||||
# the command is run by Bash with `/bin/bash -c`.
|
||||
#
|
||||
# @param ... = command to execute with Bash
|
||||
function _exec_in_container_bash() { _exec_in_container /bin/bash -c "${@}" ; }
|
||||
|
||||
# @param ${1} timeout
|
||||
# @param --fatal-test <command eval string> additional test whose failure aborts immediately
|
||||
# @param ... test to run
|
||||
function repeat_until_success_or_timeout {
|
||||
# Execute a command inside the container with name ${CONTAINER_NAME}. The command
|
||||
# is run with BATS' `run` so you can check the exit code and use `assert_`. Moreover,
|
||||
# the command is run by Bash with `/bin/bash -c`.
|
||||
#
|
||||
# @param ... = Bash command to execute
|
||||
function _run_in_container_bash() { _run_in_container /bin/bash -c "${@}" ; }
|
||||
|
||||
# ? << Functions to execute commands inside a container
|
||||
# ! -------------------------------------------------------------------
|
||||
# ? >> Functions about executing commands with timeouts
|
||||
|
||||
# Repeats a given command inside a container until the timeout is over.
|
||||
#
|
||||
# @param ${1} = timeout
|
||||
# @param ${2} = container name
|
||||
# @param ... = test command for container
|
||||
function _repeat_in_container_until_success_or_timeout() {
|
||||
local TIMEOUT="${1:?Timeout duration must be provided}"
|
||||
local CONTAINER_NAME="${2:?Container name must be provided}"
|
||||
shift 2
|
||||
|
||||
_repeat_until_success_or_timeout \
|
||||
--fatal-test "_container_is_running ${CONTAINER_NAME}" \
|
||||
"${TIMEOUT}" \
|
||||
_exec_in_container "${@}"
|
||||
}
|
||||
|
||||
# Repeats a given command until the timeout is over.
|
||||
#
|
||||
# @option --fatal-test <COMMAND EVAL STRING> = additional test whose failure aborts immediately
|
||||
# @param ${1} = timeout
|
||||
# @param ... = test to run
|
||||
function _repeat_until_success_or_timeout() {
|
||||
local FATAL_FAILURE_TEST_COMMAND
|
||||
|
||||
if [[ "${1}" == "--fatal-test" ]]; then
|
||||
FATAL_FAILURE_TEST_COMMAND="${2}"
|
||||
if [[ "${1:-}" == "--fatal-test" ]]
|
||||
then
|
||||
FATAL_FAILURE_TEST_COMMAND="${2:?Provided --fatal-test but no command}"
|
||||
shift 2
|
||||
fi
|
||||
|
||||
if ! [[ "${1}" =~ ^[0-9]+$ ]]; then
|
||||
echo "First parameter for timeout must be an integer, received \"${1}\""
|
||||
local TIMEOUT=${1:?Timeout duration must be provided}
|
||||
shift 1
|
||||
|
||||
if ! [[ "${TIMEOUT}" =~ ^[0-9]+$ ]]
|
||||
then
|
||||
echo "First parameter for timeout must be an integer, received \"${TIMEOUT}\""
|
||||
return 1
|
||||
fi
|
||||
|
||||
local TIMEOUT=${1}
|
||||
local STARTTIME=${SECONDS}
|
||||
shift 1
|
||||
|
||||
until "${@}"
|
||||
do
|
||||
if [[ -n ${FATAL_FAILURE_TEST_COMMAND} ]] && ! eval "${FATAL_FAILURE_TEST_COMMAND}"; then
|
||||
if [[ -n ${FATAL_FAILURE_TEST_COMMAND} ]] && ! eval "${FATAL_FAILURE_TEST_COMMAND}"
|
||||
then
|
||||
echo "\`${FATAL_FAILURE_TEST_COMMAND}\` failed, early aborting repeat_until_success of \`${*}\`" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
|
||||
if [[ $(( SECONDS - STARTTIME )) -gt ${TIMEOUT} ]]; then
|
||||
if [[ $(( SECONDS - STARTTIME )) -gt ${TIMEOUT} ]]
|
||||
then
|
||||
echo "Timed out on command: ${*}" >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# like repeat_until_success_or_timeout but with wrapping the command to run into `run` for later bats consumption
|
||||
# @param ${1} timeout
|
||||
# @param ... test command to run
|
||||
function run_until_success_or_timeout {
|
||||
if ! [[ ${1} =~ ^[0-9]+$ ]]; then
|
||||
echo "First parameter for timeout must be an integer, received \"${1}\""
|
||||
# Like `_repeat_until_success_or_timeout` . The command is run with BATS' `run`
|
||||
# so you can check the exit code and use `assert_`.
|
||||
#
|
||||
# @param ${1} = timeout
|
||||
# @param ... = test command to run
|
||||
function _run_until_success_or_timeout() {
|
||||
local TIMEOUT=${1:?Timeout duration must be provided}
|
||||
shift 1
|
||||
|
||||
if [[ ! ${TIMEOUT} =~ ^[0-9]+$ ]]
|
||||
then
|
||||
echo "First parameter for timeout must be an integer, received \"${TIMEOUT}\""
|
||||
return 1
|
||||
fi
|
||||
|
||||
local TIMEOUT=${1}
|
||||
local STARTTIME=${SECONDS}
|
||||
shift 1
|
||||
|
||||
until run "${@}" && [[ $status -eq 0 ]]
|
||||
until run "${@}" && [[ ${status} -eq 0 ]]
|
||||
do
|
||||
sleep 1
|
||||
|
||||
if (( SECONDS - STARTTIME > TIMEOUT )); then
|
||||
if (( SECONDS - STARTTIME > TIMEOUT ))
|
||||
then
|
||||
echo "Timed out on command: ${*}" >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# @param ${1} timeout
|
||||
# @param ${2} container name
|
||||
# @param ... test command for container
|
||||
function repeat_in_container_until_success_or_timeout() {
|
||||
local TIMEOUT="${1}"
|
||||
local CONTAINER_NAME="${2}"
|
||||
shift 2
|
||||
# ? << Functions about executing commands with timeouts
|
||||
# ! -------------------------------------------------------------------
|
||||
# ? >> Functions to wait until a condition is met
|
||||
|
||||
repeat_until_success_or_timeout --fatal-test "container_is_running ${CONTAINER_NAME}" "${TIMEOUT}" docker exec "${CONTAINER_NAME}" "${@}"
|
||||
|
||||
# Wait until a port is ready.
|
||||
#
|
||||
# @param ${1} = port
|
||||
# @param ${2} = container name [OPTIONAL]
|
||||
function _wait_for_tcp_port_in_container() {
|
||||
local PORT=${1:?Port number must be provided}
|
||||
local CONTAINER_NAME=$(__handle_container_name "${2:-}")
|
||||
|
||||
_repeat_until_success_or_timeout \
|
||||
--fatal-test "_container_is_running ${CONTAINER_NAME}" \
|
||||
"${TEST_TIMEOUT_IN_SECONDS}" \
|
||||
_exec_in_container_bash "nc -z 0.0.0.0 ${PORT}"
|
||||
}
|
||||
|
||||
function container_is_running() {
|
||||
[[ "$(docker inspect -f '{{.State.Running}}' "${1}")" == "true" ]]
|
||||
# Wait for SMTP port (25) to become ready.
|
||||
#
|
||||
# @param ${1} = name of the container [OPTIONAL]
|
||||
function _wait_for_smtp_port_in_container() {
|
||||
local CONTAINER_NAME=$(__handle_container_name "${1:-}")
|
||||
_wait_for_tcp_port_in_container 25
|
||||
}
|
||||
|
||||
# @param ${1} port
|
||||
# @param ${2} container name
|
||||
function wait_for_tcp_port_in_container() {
|
||||
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}"
|
||||
}
|
||||
# Wait until the SMPT port (25) can respond.
|
||||
#
|
||||
# @param ${1} = name of the container [OPTIONAL]
|
||||
function _wait_for_smtp_port_in_container_to_respond() {
|
||||
local CONTAINER_NAME=$(__handle_container_name "${1:-}")
|
||||
|
||||
# @param ${1} name of the postfix container
|
||||
function wait_for_smtp_port_in_container() {
|
||||
wait_for_tcp_port_in_container 25 "${1}"
|
||||
}
|
||||
|
||||
# @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 ]]
|
||||
until [[ $(_exec_in_container timeout 10 /bin/bash -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))
|
||||
(( COUNT += 1 ))
|
||||
done
|
||||
}
|
||||
|
||||
# @param ${1} name of the postfix container
|
||||
function wait_for_amavis_port_in_container() {
|
||||
wait_for_tcp_port_in_container 10024 "${1}"
|
||||
# Checks whether a service is running inside a container (${1}).
|
||||
#
|
||||
# @param ${1} = service name
|
||||
# @param ${2} = container name [OPTIONAL]
|
||||
function _should_have_service_running_in_container() {
|
||||
local SERVICE_NAME="${1:?Service name must be provided}"
|
||||
local CONTAINER_NAME=$(__handle_container_name "${2:-}")
|
||||
|
||||
_run_in_container /usr/bin/supervisorctl status "${SERVICE_NAME}"
|
||||
assert_success
|
||||
assert_output --partial 'RUNNING'
|
||||
}
|
||||
|
||||
# get the private config path for the given container or test file, if no container name was given
|
||||
function private_config_path() {
|
||||
echo "${PWD}/test/duplicate_configs/${1:-$(basename "${BATS_TEST_FILENAME}")}"
|
||||
}
|
||||
# Wait until a service is running.
|
||||
#
|
||||
# @param ${1} = name of the service to wait for
|
||||
# @param ${2} = container name [OPTIONAL]
|
||||
function _wait_for_service() {
|
||||
local SERVICE_NAME="${1:?Service name must be provided}"
|
||||
local CONTAINER_NAME=$(__handle_container_name "${2:-}")
|
||||
|
||||
function container_has_service_running() {
|
||||
local CONTAINER_NAME="${1}"
|
||||
local SERVICE_NAME="${2}"
|
||||
|
||||
docker exec "${CONTAINER_NAME}" /usr/bin/supervisorctl status "${SERVICE_NAME}" | grep RUNNING >/dev/null
|
||||
}
|
||||
|
||||
function wait_for_service() {
|
||||
local CONTAINER_NAME="${1}"
|
||||
local SERVICE_NAME="${2}"
|
||||
|
||||
repeat_until_success_or_timeout --fatal-test "container_is_running ${CONTAINER_NAME}" "${TEST_TIMEOUT_IN_SECONDS}" \
|
||||
container_has_service_running "${CONTAINER_NAME}" "${SERVICE_NAME}"
|
||||
}
|
||||
|
||||
# NOTE: Relies on ENV `LOG_LEVEL=debug` or higher
|
||||
function _wait_until_expected_count_is_matched() {
|
||||
function __get_count() {
|
||||
# NOTE: `|| true` required due to `set -e` usage:
|
||||
# https://github.com/docker-mailserver/docker-mailserver/pull/2997#discussion_r1070583876
|
||||
docker exec "${CONTAINER_NAME}" grep --count "${MATCH_CONTENT}" "${MATCH_IN_LOG}" || true
|
||||
}
|
||||
|
||||
# WARNING: Keep in mind it is a '>=' comparison.
|
||||
# If you provide an explict count to match, ensure it is not too low to cause a false-positive.
|
||||
function __has_expected_count() {
|
||||
[[ $(__get_count) -ge "${EXPECTED_COUNT}" ]]
|
||||
}
|
||||
|
||||
local CONTAINER_NAME=${1}
|
||||
local EXPECTED_COUNT=${2}
|
||||
|
||||
# Ensure early failure if arg is missing:
|
||||
assert_not_equal "${CONTAINER_NAME}" ''
|
||||
|
||||
# Ensure the container is configured with the required `LOG_LEVEL` ENV:
|
||||
assert_regex \
|
||||
$(docker exec "${CONTAINER_NAME}" env | grep '^LOG_LEVEL=') \
|
||||
'=(debug|trace)$'
|
||||
|
||||
# Default behaviour is to wait until one new match is found (eg: incremented),
|
||||
# unless explicitly set (useful for waiting on a min count to be reached):
|
||||
if [[ -z $EXPECTED_COUNT ]]
|
||||
then
|
||||
# +1 of starting count:
|
||||
EXPECTED_COUNT=$(( $(__get_count) + 1 ))
|
||||
fi
|
||||
|
||||
repeat_until_success_or_timeout 20 __has_expected_count
|
||||
_repeat_until_success_or_timeout \
|
||||
--fatal-test "_container_is_running ${CONTAINER_NAME}" \
|
||||
"${TEST_TIMEOUT_IN_SECONDS}" \
|
||||
_should_have_service_running_in_container "${SERVICE_NAME}"
|
||||
}
|
||||
|
||||
# An account added to `postfix-accounts.cf` must wait for the `changedetector` service
|
||||
# to process the update before Dovecot creates the mail account and associated storage dir:
|
||||
function wait_until_account_maildir_exists() {
|
||||
local CONTAINER_NAME=$1
|
||||
local MAIL_ACCOUNT=$2
|
||||
# to process the update before Dovecot creates the mail account and associated storage dir.
|
||||
#
|
||||
# @param ${1} = mail account name
|
||||
# @param ${2} = container name
|
||||
function _wait_until_account_maildir_exists() {
|
||||
local MAIL_ACCOUNT=${1:?Mail account must be provided}
|
||||
local CONTAINER_NAME=$(__handle_container_name "${2:-}")
|
||||
|
||||
local LOCAL_PART="${MAIL_ACCOUNT%@*}"
|
||||
local DOMAIN_PART="${MAIL_ACCOUNT#*@}"
|
||||
local MAIL_ACCOUNT_STORAGE_DIR="/var/mail/${DOMAIN_PART}/${LOCAL_PART}"
|
||||
|
||||
repeat_in_container_until_success_or_timeout 60 "${CONTAINER_NAME}" bash -c "[[ -d ${MAIL_ACCOUNT_STORAGE_DIR} ]]"
|
||||
_repeat_in_container_until_success_or_timeout 60 "${CONTAINER_NAME}" \
|
||||
/bin/bash -c "[[ -d ${MAIL_ACCOUNT_STORAGE_DIR} ]]"
|
||||
}
|
||||
|
||||
function add_mail_account_then_wait_until_ready() {
|
||||
local CONTAINER_NAME=$1
|
||||
local MAIL_ACCOUNT=$2
|
||||
# Password is optional (omit when the password is not needed during the test)
|
||||
local MAIL_PASS="${3:-password_not_relevant_to_test}"
|
||||
|
||||
run docker exec "${CONTAINER_NAME}" setup email add "${MAIL_ACCOUNT}" "${MAIL_PASS}"
|
||||
assert_success
|
||||
|
||||
wait_until_account_maildir_exists "${CONTAINER_NAME}" "${MAIL_ACCOUNT}"
|
||||
}
|
||||
|
||||
function wait_for_empty_mail_queue_in_container() {
|
||||
local CONTAINER_NAME="${1}"
|
||||
# Wait until the mail queue is empty inside a container (${1}).
|
||||
#
|
||||
# @param ${1} = container name [OPTIONAL]
|
||||
function _wait_for_empty_mail_queue_in_container() {
|
||||
local CONTAINER_NAME=$(__handle_container_name "${1:-}")
|
||||
local TIMEOUT=${TEST_TIMEOUT_IN_SECONDS}
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
repeat_in_container_until_success_or_timeout "${TIMEOUT}" "${CONTAINER_NAME}" bash -c '[[ $(mailq) == *"Mail queue is empty"* ]]'
|
||||
_repeat_in_container_until_success_or_timeout \
|
||||
"${TIMEOUT}" \
|
||||
"${CONTAINER_NAME}" \
|
||||
/bin/bash -c '[[ $(mailq) == "Mail queue is empty" ]]'
|
||||
}
|
||||
|
||||
# `lines` is a special BATS variable updated via `run`:
|
||||
function _should_output_number_of_lines() {
|
||||
assert_equal "${#lines[@]}" $1
|
||||
|
||||
# ? << Functions to wait until a condition is met
|
||||
# ! -------------------------------------------------------------------
|
||||
# ? >> Miscellaneous helper functions
|
||||
|
||||
# Adds a mail account and waits for the associated files to be created.
|
||||
#
|
||||
# @param ${1} = mail account name
|
||||
# @param ${2} = password [OPTIONAL]
|
||||
# @param ${3} = container name [OPTIONAL]
|
||||
function _add_mail_account_then_wait_until_ready() {
|
||||
local MAIL_ACCOUNT=${1:?Mail account must be provided}
|
||||
local MAIL_PASS="${2:-password_not_relevant_to_test}"
|
||||
local CONTAINER_NAME=$(__handle_container_name "${3:-}")
|
||||
|
||||
_run_in_container setup email add "${MAIL_ACCOUNT}" "${MAIL_PASS}"
|
||||
assert_success
|
||||
|
||||
_wait_until_account_maildir_exists "${MAIL_ACCOUNT}"
|
||||
}
|
||||
|
||||
# Assert that the number of lines output by a previous command matches the given
|
||||
# amount (${1}). `lines` is a special BATS variable updated via `run`.
|
||||
#
|
||||
# @param ${1} = number of lines that the output should have
|
||||
function _should_output_number_of_lines() {
|
||||
assert_equal "${#lines[@]}" "${1:?Number of lines not provided}"
|
||||
}
|
||||
|
||||
# Reloads the postfix service.
|
||||
#
|
||||
# @param ${1} = container name [OPTIONAL]
|
||||
function _reload_postfix() {
|
||||
local CONTAINER_NAME=$(__handle_container_name "${1:-}")
|
||||
|
||||
# Reloading Postfix config after modifying it within 2 seconds will cause Postfix to delay reading `main.cf`:
|
||||
# WORKAROUND: https://github.com/docker-mailserver/docker-mailserver/pull/2998
|
||||
_exec_in_container touch -d '2 seconds ago' /etc/postfix/main.cf
|
||||
_exec_in_container postfix reload
|
||||
}
|
||||
|
||||
|
||||
# Get the IP of the container (${1}).
|
||||
#
|
||||
# @param ${1} = container name [OPTIONAL]
|
||||
function _get_container_ip() {
|
||||
local TARGET_CONTAINER_NAME=$(__handle_container_name "${1:-}")
|
||||
docker inspect --format '{{ .NetworkSettings.IPAddress }}' "${TARGET_CONTAINER_NAME}"
|
||||
}
|
||||
|
||||
# Check if a container is running.
|
||||
#
|
||||
# @param ${1} = container name [OPTIONAL]
|
||||
function _container_is_running() {
|
||||
local TARGET_CONTAINER_NAME=$(__handle_container_name "${1:-}")
|
||||
[[ $(docker inspect -f '{{.State.Running}}' "${TARGET_CONTAINER_NAME}") == 'true' ]]
|
||||
}
|
||||
|
||||
# Checks if the directory exists and then how many files it contains at the top-level.
|
||||
#
|
||||
# @param ${1} = directory
|
||||
# @param ${2} = number of files that should be in ${1}
|
||||
# @param ${3} = container name [OPTIONAL]
|
||||
function _count_files_in_directory_in_container()
|
||||
{
|
||||
local DIRECTORY=${1:?No directory provided}
|
||||
local NUMBER_OF_LINES=${2:?No line count provided}
|
||||
local CONTAINER_NAME=$(__handle_container_name "${3:-}")
|
||||
|
||||
_run_in_container_bash "[[ -d ${DIRECTORY} ]]"
|
||||
assert_success
|
||||
|
||||
_run_in_container_bash "find ${DIRECTORY} -maxdepth 1 -type f -printf 'x\n'"
|
||||
assert_success
|
||||
_should_output_number_of_lines "${NUMBER_OF_LINES}"
|
||||
}
|
||||
|
||||
# ? << Miscellaneous helper functions
|
||||
# ! -------------------------------------------------------------------
|
||||
|
@ -1,7 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# ? ABOUT: Functions defined here should be used when initializing tests.
|
||||
|
||||
# ! ATTENTION: Functions prefixed with `__` are intended for internal use within this file only, not in tests.
|
||||
# ! ATTENTION: This script must not use functions from `common.bash` to
|
||||
# ! avoid dependency hell.
|
||||
|
||||
# ! -------------------------------------------------------------------
|
||||
# ? >> Miscellaneous initialization functionality
|
||||
|
||||
# Does pre-flight checks for each test: check whether certain required variables
|
||||
# are set and exports other variables.
|
||||
#
|
||||
# ## Note
|
||||
#
|
||||
# This function is internal and should not be used in tests.
|
||||
function __initialize_variables() {
|
||||
function __check_if_set() {
|
||||
if [[ ${!1+set} != 'set' ]]
|
||||
@ -28,57 +41,70 @@ function __initialize_variables() {
|
||||
NUMBER_OF_LOG_LINES=${NUMBER_OF_LOG_LINES:-10}
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# ? << Miscellaneous initialization functionality
|
||||
# ! -------------------------------------------------------------------
|
||||
# ? >> File setup
|
||||
|
||||
# @param ${1} relative source in test/config folder
|
||||
# @param ${2} (optional) container name, defaults to ${BATS_TEST_FILENAME}
|
||||
# @return path to the folder where the config is duplicated
|
||||
function duplicate_config_for_container() {
|
||||
# Print the private config path for the given container or test file,
|
||||
# if no container name was given.
|
||||
#
|
||||
# @param ${1} = container name [OPTIONAL]
|
||||
function _print_private_config_path() {
|
||||
local TARGET_NAME=${1:-$(basename "${BATS_TEST_FILENAME}")}
|
||||
echo "${REPOSITORY_ROOT}/test/duplicate_configs/${TARGET_NAME}"
|
||||
}
|
||||
|
||||
|
||||
# Create a dedicated configuration directory for a test file.
|
||||
#
|
||||
# @param ${1} = relative source in test/config folder
|
||||
# @param ${2} = (optional) container name, defaults to ${BATS_TEST_FILENAME}
|
||||
# @return = path to the folder where the config is duplicated
|
||||
function _duplicate_config_for_container() {
|
||||
local OUTPUT_FOLDER
|
||||
OUTPUT_FOLDER=$(private_config_path "${2}") || return $?
|
||||
OUTPUT_FOLDER=$(_print_private_config_path "${2}")
|
||||
|
||||
rm -rf "${OUTPUT_FOLDER:?}/" || return $? # cleanup
|
||||
mkdir -p "${OUTPUT_FOLDER}" || return $?
|
||||
cp -r "${PWD}/test/config/${1:?}/." "${OUTPUT_FOLDER}" || return $?
|
||||
if [[ -z ${OUTPUT_FOLDER} ]]
|
||||
then
|
||||
echo "'OUTPUT_FOLDER' in '_duplicate_config_for_container' is empty" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -rf "${OUTPUT_FOLDER:?}/"
|
||||
mkdir -p "${OUTPUT_FOLDER}"
|
||||
cp -r "${REPOSITORY_ROOT}/test/config/${1:?}/." "${OUTPUT_FOLDER}" || return $?
|
||||
|
||||
echo "${OUTPUT_FOLDER}"
|
||||
}
|
||||
|
||||
# TODO: Should also fail early on "docker logs ${1} | egrep '^[ FATAL ]'"?
|
||||
# @param ${1} name of the postfix container
|
||||
function wait_for_finished_setup_in_container() {
|
||||
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
|
||||
echo "Last ${NUMBER_OF_LOG_LINES} lines of container \`${1}\`'s log"
|
||||
docker logs "${1}" | tail -n "${NUMBER_OF_LOG_LINES}"
|
||||
fi
|
||||
|
||||
return ${STATUS}
|
||||
}
|
||||
|
||||
# Common defaults appropriate for most tests, override vars in each test when necessary.
|
||||
# For all tests override in `setup_file()` via an `export` var.
|
||||
# For individual test override the var via `local` var instead.
|
||||
# Common defaults appropriate for most tests.
|
||||
#
|
||||
# Override variables in test cases within a file when necessary:
|
||||
# - Use `export <VARIABLE>` in `setup_file()` to overrides for all test cases.
|
||||
# - Use `local <VARIABLE>` to override within a specific test case.
|
||||
#
|
||||
# ## Attenton
|
||||
#
|
||||
# The ENV `CONTAINER_NAME` must be set before this method is called. It only affects the
|
||||
# `TEST_TMP_CONFIG` directory created, but will be used in `common_container_create()`
|
||||
# and implicitly in other helper methods.
|
||||
#
|
||||
# ## Example
|
||||
#
|
||||
# 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}")`
|
||||
#
|
||||
# REQUIRED: `CONTAINER_NAME` must be set before this method is called.
|
||||
# It only affects the `TEST_TMP_CONFIG` directory created,
|
||||
# but will be used in `common_container_create()` and implicitly in other helper methods.
|
||||
function init_with_defaults() {
|
||||
# in the file, then use `local TEST_TMP_CONFIG=$(_duplicate_config_for_container . "${UNIQUE_ID_HERE}")`
|
||||
function _init_with_defaults() {
|
||||
__initialize_variables
|
||||
|
||||
export TEST_TMP_CONFIG
|
||||
TEST_TMP_CONFIG=$(duplicate_config_for_container . "${CONTAINER_NAME}")
|
||||
TEST_TMP_CONFIG=$(_duplicate_config_for_container . "${CONTAINER_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="${REPOSITORY_ROOT}/test/test-files:${TEST_FILES_CONTAINER_PATH}:ro"
|
||||
|
||||
# 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)
|
||||
@ -89,22 +115,43 @@ function init_with_defaults() {
|
||||
export TEST_CA_CERT="${TEST_FILES_CONTAINER_PATH}/ssl/example.test/with_ca/ecdsa/ca-cert.ecdsa.pem"
|
||||
}
|
||||
|
||||
# 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.
|
||||
function common_container_setup() {
|
||||
common_container_create "${@}"
|
||||
common_container_start
|
||||
|
||||
# ? << File setup
|
||||
# ! -------------------------------------------------------------------
|
||||
# ? >> Container startup
|
||||
|
||||
# Waits until the container has finished starting up.
|
||||
#
|
||||
# @param ${1} = container name
|
||||
#
|
||||
# TODO: Should also fail early on "docker logs ${1} | egrep '^[ FATAL ]'"?
|
||||
function _wait_for_finished_setup_in_container() {
|
||||
local TARGET_CONTAINER_NAME=${1:?Container name must be provided}
|
||||
local STATUS=0
|
||||
_repeat_until_success_or_timeout \
|
||||
--fatal-test "_container_is_running ${1}" \
|
||||
"${TEST_TIMEOUT_IN_SECONDS}" \
|
||||
bash -c "docker logs ${TARGET_CONTAINER_NAME} | grep 'is up and running'" || STATUS=1
|
||||
|
||||
if [[ ${STATUS} -eq 1 ]]; then
|
||||
echo "Last ${NUMBER_OF_LOG_LINES} lines of container (${TARGET_CONTAINER_NAME}) log"
|
||||
docker logs "${1}" | tail -n "${NUMBER_OF_LOG_LINES}"
|
||||
fi
|
||||
|
||||
return "${STATUS}"
|
||||
}
|
||||
|
||||
# Common docker setup is centralized here.
|
||||
# Uses `docker create` to create a container with proper defaults without starting it instantly.
|
||||
#
|
||||
# `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:
|
||||
# @param ${1} = 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 [OPTIONAL]
|
||||
#
|
||||
# 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() {
|
||||
# ## 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() {
|
||||
[[ -n ${1} ]] && local -n X_EXTRA_ARGS=${1}
|
||||
|
||||
run docker create \
|
||||
@ -127,9 +174,34 @@ function common_container_create() {
|
||||
assert_success
|
||||
}
|
||||
|
||||
function common_container_start() {
|
||||
run docker start "${CONTAINER_NAME}"
|
||||
# Starts a container given by it's name.
|
||||
# Uses `CONTAINER_NAME` as the name for the `docker start` command.
|
||||
#
|
||||
# ## Attenton
|
||||
#
|
||||
# The ENV `CONTAINER_NAME` must be set before this method is called.
|
||||
function _common_container_start() {
|
||||
run docker start "${CONTAINER_NAME:?Container name must be set}"
|
||||
assert_success
|
||||
|
||||
wait_for_finished_setup_in_container "${CONTAINER_NAME}"
|
||||
_wait_for_finished_setup_in_container "${CONTAINER_NAME}"
|
||||
}
|
||||
|
||||
# Using `create` and `start` instead of only `run` allows to modify
|
||||
# the container prior to starting it. Otherwise use this combined method.
|
||||
#
|
||||
# ## Note
|
||||
#
|
||||
# This function forwards all arguments to `_common_container_create` at present.
|
||||
function _common_container_setup() {
|
||||
_common_container_create "${@}"
|
||||
_common_container_start
|
||||
}
|
||||
|
||||
# Can be used in BATS' `teardown_file` function as a default value.
|
||||
#
|
||||
# @param ${1} = container name [OPTIONAL]
|
||||
function _default_teardown() {
|
||||
local TARGET_CONTAINER_NAME=${1:-${CONTAINER_NAME}}
|
||||
docker rm -f "${TARGET_CONTAINER_NAME}"
|
||||
}
|
||||
|
@ -1,10 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
# TODO: Functions need better documentation / or documentation at all (adhere to doc conventions!)
|
||||
# ? ABOUT: Functions defined here can be used when testing encrypt-related functionality.
|
||||
# ? NOTE: `_should_*` methods are useful for common high-level functionality.
|
||||
|
||||
# ! -------------------------------------------------------------------
|
||||
# ? >> Miscellaneous initialization functionality
|
||||
|
||||
load "${REPOSITORY_ROOT}/test/helper/common"
|
||||
|
||||
# `_should_*` methods are useful for common high-level functionality.
|
||||
|
||||
# ? --------------------------------------------- Negotiate TLS
|
||||
# ? << Miscellaneous initialization functionality
|
||||
# ! -------------------------------------------------------------------
|
||||
# ? >> Negotiate TLS
|
||||
|
||||
# For certs actually provisioned from LetsEncrypt the Root CA cert should not need to be provided,
|
||||
# as it would already be available by default in `/etc/ssl/certs`, requiring only the cert chain (fullchain.pem).
|
||||
@ -14,11 +21,12 @@ function _should_succesfully_negotiate_tls() {
|
||||
local CA_CERT=${2:-${TEST_CA_CERT}}
|
||||
|
||||
# Postfix and Dovecot are ready:
|
||||
wait_for_smtp_port_in_container_to_respond "${CONTAINER_NAME}"
|
||||
wait_for_tcp_port_in_container 993 "${CONTAINER_NAME}"
|
||||
_wait_for_smtp_port_in_container_to_respond
|
||||
_wait_for_tcp_port_in_container 993
|
||||
|
||||
# Root CA cert should be present in the container:
|
||||
assert docker exec "${CONTAINER_NAME}" [ -f "${CA_CERT}" ]
|
||||
_run_in_container_bash "[[ -f ${CA_CERT} ]]"
|
||||
assert_success
|
||||
|
||||
local PORTS=(25 587 465 143 993)
|
||||
for PORT in "${PORTS[@]}"
|
||||
@ -82,7 +90,7 @@ function _generate_openssl_cmd() {
|
||||
|
||||
function _get_fqdn_match_query() {
|
||||
local FQDN
|
||||
FQDN=$(escape_fqdn "${1}")
|
||||
FQDN=$(_escape_fqdn "${1}")
|
||||
|
||||
# 3rd check is for wildcard support by replacing the 1st DNS label of the FQDN with a `*`,
|
||||
# eg: `mail.example.test` will become `*.example.test` matching `DNS:*.example.test`.
|
||||
@ -101,7 +109,7 @@ function _should_not_support_fqdn_in_cert() {
|
||||
|
||||
# Escapes `*` and `.` so the FQDN literal can be used in regex queries
|
||||
# `sed` will match those two chars and `\\&` says to prepend a `\` to the sed match (`&`)
|
||||
function escape_fqdn() {
|
||||
function _escape_fqdn() {
|
||||
# shellcheck disable=SC2001
|
||||
sed 's|[\*\.]|\\&|g' <<< "${1}"
|
||||
}
|
||||
|
Reference in New Issue
Block a user