mirror of
https://github.com/docker-mailserver/docker-mailserver
synced 2024-12-18 14:04:17 +01:00
438 lines
14 KiB
Bash
438 lines
14 KiB
Bash
#!/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"
|
|
load "${REPOSITORY_ROOT}/test/helper/sending"
|
|
load "${REPOSITORY_ROOT}/test/helper/log_and_filtering"
|
|
}
|
|
|
|
__load_bats_helper
|
|
|
|
# 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
|
|
}
|
|
|
|
# ? << 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
|
|
docker exec "${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}" "${@}"
|
|
}
|
|
|
|
# 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}" "${@}"
|
|
}
|
|
|
|
# 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 "${@}" ; }
|
|
|
|
# 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 "${@}" ; }
|
|
|
|
# Run a command in Bash and filter the output given a regex.
|
|
#
|
|
# @param ${1} = command to run in Bash
|
|
# @param ${2} = regex to filter [OPTIONAL]
|
|
#
|
|
# ## Attention
|
|
#
|
|
# The regex is given to `grep -E`, so make sure it is compatible.
|
|
#
|
|
# ## Note
|
|
#
|
|
# If no regex is provided, this function will default to one that strips
|
|
# empty lines and Bash comments from the output.
|
|
function _run_in_container_bash_and_filter_output() {
|
|
local COMMAND=${1:?Command must be provided}
|
|
local FILTER_REGEX=${2:-^[[:space:]]*$|^ *#}
|
|
|
|
_run_in_container_bash "${COMMAND} | grep -E -v '${FILTER_REGEX}'"
|
|
assert_success
|
|
}
|
|
|
|
# ? << 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:?Provided --fatal-test but no command}"
|
|
shift 2
|
|
fi
|
|
|
|
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 STARTTIME=${SECONDS}
|
|
|
|
until "${@}"; do
|
|
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
|
|
echo "Timed out on command: ${*}" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
# 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 STARTTIME=${SECONDS}
|
|
|
|
# shellcheck disable=SC2154
|
|
until run "${@}" && [[ ${status} -eq 0 ]]; do
|
|
sleep 1
|
|
|
|
if (( SECONDS - STARTTIME > TIMEOUT )); then
|
|
echo "Timed out on command: ${*}" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ? << Functions about executing commands with timeouts
|
|
# ! -------------------------------------------------------------------
|
|
# ? >> Functions to wait until a condition is met
|
|
|
|
# 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}"
|
|
}
|
|
|
|
# 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
|
|
}
|
|
|
|
# Wait until the SMTP 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:-}")
|
|
|
|
local COUNT=0
|
|
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 ))
|
|
done
|
|
}
|
|
|
|
# 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'
|
|
}
|
|
|
|
# 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:-}")
|
|
|
|
_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.
|
|
#
|
|
# @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}" \
|
|
/bin/bash -c "[[ -d ${MAIL_ACCOUNT_STORAGE_DIR} ]]"
|
|
}
|
|
|
|
# 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}" \
|
|
/bin/bash -c '[[ $(mailq) == "Mail queue is empty" ]]'
|
|
}
|
|
|
|
|
|
# ? << 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:-}")
|
|
|
|
# Required to detect a new account and create the maildir:
|
|
_wait_for_service changedetector "${CONTAINER_NAME}"
|
|
|
|
_run_in_container setup email add "${MAIL_ACCOUNT}" "${MAIL_PASS}"
|
|
assert_success
|
|
|
|
_wait_until_account_maildir_exists "${MAIL_ACCOUNT}"
|
|
}
|
|
|
|
# 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}
|
|
function _count_files_in_directory_in_container() {
|
|
local DIRECTORY=${1:?No directory provided}
|
|
local NUMBER_OF_LINES=${2:?No line count provided}
|
|
|
|
_should_have_content_in_directory "${DIRECTORY}" '-type f'
|
|
_should_output_number_of_lines "${NUMBER_OF_LINES}"
|
|
}
|
|
|
|
# Checks if the directory exists and then list the top-level content.
|
|
#
|
|
# @param ${1} = directory
|
|
# @param ${2} = Additional options to `find`
|
|
function _should_have_content_in_directory() {
|
|
local DIRECTORY=${1:?No directory provided}
|
|
local FIND_OPTIONS=${2:-}
|
|
|
|
_run_in_container_bash "[[ -d ${DIRECTORY} ]] && find ${DIRECTORY} -mindepth 1 -maxdepth 1 ${FIND_OPTIONS} -printf '%f\n'"
|
|
assert_success
|
|
}
|
|
|
|
# A simple wrapper for netcat (`nc`). This is useful when sending
|
|
# "raw" e-mails or doing IMAP-related work.
|
|
#
|
|
# @param ${1} = the file that is given to `nc`
|
|
# @param ${1} = custom parameters for `nc` [OPTIONAL] (default: 0.0.0.0 25)
|
|
function _nc_wrapper() {
|
|
local FILE=${1:?Must provide name of template file}
|
|
local NC_PARAMETERS=${2:-0.0.0.0 25}
|
|
|
|
[[ -v CONTAINER_NAME ]] || return 1
|
|
|
|
_run_in_container_bash "nc ${NC_PARAMETERS} < /tmp/docker-mailserver-test/${FILE}"
|
|
}
|
|
|
|
# A simple wrapper for a test that checks whether a file exists.
|
|
#
|
|
# @param ${1} = the path to the file inside the container
|
|
function _file_exists_in_container() {
|
|
_run_in_container_bash "[[ -f ${1} ]]"
|
|
assert_success
|
|
}
|
|
|
|
# A simple wrapper for a test that checks whether a file does not exist.
|
|
#
|
|
# @param ${1} = the path to the file (that should not exists) inside the container
|
|
function _file_does_not_exist_in_container() {
|
|
_run_in_container_bash "[[ -f ${1} ]]"
|
|
assert_failure
|
|
}
|
|
|
|
# ? << Miscellaneous helper functions
|
|
# ! -------------------------------------------------------------------
|