diff --git a/Makefile b/Makefile index c83e9fad..01d7ffd6 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ fixtures: tests: # Start tests - /bin/bash ./test/test.sh + ./test/bats/bats test/tests.bats clean: # Get default files back diff --git a/assert.sh b/assert.sh deleted file mode 100644 index ffd2b955..00000000 --- a/assert.sh +++ /dev/null @@ -1,186 +0,0 @@ -#!/bin/bash -# assert.sh 1.1 - bash unit testing framework -# Copyright (C) 2009-2015 Robert Lehmann -# -# http://github.com/lehmannro/assert.sh -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - -export DISCOVERONLY=${DISCOVERONLY:-} -export DEBUG=${DEBUG:-} -export STOP=${STOP:-} -export INVARIANT=${INVARIANT:-} -export CONTINUE=${CONTINUE:-} - -args="$(getopt -n "$0" -l \ - verbose,help,stop,discover,invariant,continue vhxdic $*)" \ -|| exit -1 -for arg in $args; do - case "$arg" in - -h) - echo "$0 [-vxidc]" \ - "[--verbose] [--stop] [--invariant] [--discover] [--continue]" - echo "`sed 's/./ /g' <<< "$0"` [-h] [--help]" - exit 0;; - --help) - cat < [stdin] - (( tests_ran++ )) || : - [[ -z "$DISCOVERONLY" ]] || return - expected=$(echo -ne "${2:-}") - result="$(eval 2>/dev/null $1 <<< ${3:-})" || true - if [[ "$result" == "$expected" ]]; then - [[ -z "$DEBUG" ]] || echo -n . - return - fi - result="$(sed -e :a -e '$!N;s/\n/\\n/;ta' <<< "$result")" - [[ -z "$result" ]] && result="nothing" || result="\"$result\"" - [[ -z "$2" ]] && expected="nothing" || expected="\"$2\"" - _assert_fail "expected $expected${_indent}got $result" "$1" "$3" -} - -assert_raises() { - # assert_raises [stdin] - (( tests_ran++ )) || : - [[ -z "$DISCOVERONLY" ]] || return - status=0 - (eval $1 <<< ${3:-}) > /dev/null 2>&1 || status=$? - expected=${2:-0} - if [[ "$status" -eq "$expected" ]]; then - [[ -z "$DEBUG" ]] || echo -n . - return - fi - _assert_fail "program terminated with code $status instead of $expected" "$1" "$3" -} - -_assert_fail() { - # _assert_fail - [[ -n "$DEBUG" ]] && echo -n X - report="test #$tests_ran \"$2${3:+ <<< $3}\" failed:${_indent}$1" - if [[ -n "$STOP" ]]; then - [[ -n "$DEBUG" ]] && echo - echo "$report" - exit 1 - fi - tests_errors[$tests_failed]="$report" - (( tests_failed++ )) || : -} - -skip_if() { - # skip_if - (eval $@) > /dev/null 2>&1 && status=0 || status=$? - [[ "$status" -eq 0 ]] || return - skip -} - -skip() { - # skip (no arguments) - shopt -q extdebug && tests_extdebug=0 || tests_extdebug=1 - shopt -q -o errexit && tests_errexit=0 || tests_errexit=1 - # enable extdebug so returning 1 in a DEBUG trap handler skips next command - shopt -s extdebug - # disable errexit (set -e) so we can safely return 1 without causing exit - set +o errexit - tests_trapped=0 - trap _skip DEBUG -} -_skip() { - if [[ $tests_trapped -eq 0 ]]; then - # DEBUG trap for command we want to skip. Do not remove the handler - # yet because *after* the command we need to reset extdebug/errexit (in - # another DEBUG trap.) - tests_trapped=1 - [[ -z "$DEBUG" ]] || echo -n s - return 1 - else - trap - DEBUG - [[ $tests_extdebug -eq 0 ]] || shopt -u extdebug - [[ $tests_errexit -eq 1 ]] || set -o errexit - return 0 - fi -} - - -_assert_reset -: ${tests_suite_status:=0} # remember if any of the tests failed so far -_assert_cleanup() { - local status=$? - # modify exit code if it's not already non-zero - [[ $status -eq 0 && -z $CONTINUE ]] && exit $tests_suite_status -} -trap _assert_cleanup EXIT diff --git a/test/auth/smtp-auth-plain-wrong.txt b/test/auth/smtp-auth-plain-wrong.txt new file mode 100644 index 00000000..d8d8ad2a --- /dev/null +++ b/test/auth/smtp-auth-plain-wrong.txt @@ -0,0 +1,3 @@ +EHLO mail +AUTH PLAIN WRONGPASSWORD +QUIT diff --git a/test/bats/bats b/test/bats/bats new file mode 100755 index 00000000..7e1c9eda --- /dev/null +++ b/test/bats/bats @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +set -e + +version() { + echo "Bats 0.4.0" +} + +usage() { + version + echo "Usage: bats [-c] [-p | -t] [ ...]" +} + +help() { + usage + echo + echo " is the path to a Bats test file, or the path to a directory" + echo " containing Bats test files." + echo + echo " -c, --count Count the number of test cases without running any tests" + echo " -h, --help Display this help message" + echo " -p, --pretty Show results in pretty format (default for terminals)" + echo " -t, --tap Show results in TAP format" + echo " -v, --version Display the version number" + echo + echo " For more information, see https://github.com/sstephenson/bats" + echo +} + +resolve_link() { + $(type -p greadlink readlink | head -1) "$1" +} + +abs_dirname() { + local cwd="$(pwd)" + local path="$1" + + while [ -n "$path" ]; do + cd "${path%/*}" + local name="${path##*/}" + path="$(resolve_link "$name" || true)" + done + + pwd + cd "$cwd" +} + +expand_path() { + { cd "$(dirname "$1")" 2>/dev/null + local dirname="$PWD" + cd "$OLDPWD" + echo "$dirname/$(basename "$1")" + } || echo "$1" +} + +BATS_LIBEXEC="$(abs_dirname "$0")" +export BATS_PREFIX="$(abs_dirname "$BATS_LIBEXEC")" +export BATS_CWD="$(abs_dirname .)" +export PATH="$BATS_LIBEXEC:$PATH" + +options=() +arguments=() +for arg in "$@"; do + if [ "${arg:0:1}" = "-" ]; then + if [ "${arg:1:1}" = "-" ]; then + options[${#options[*]}]="${arg:2}" + else + index=1 + while option="${arg:$index:1}"; do + [ -n "$option" ] || break + options[${#options[*]}]="$option" + let index+=1 + done + fi + else + arguments[${#arguments[*]}]="$arg" + fi +done + +unset count_flag pretty +[ -t 0 ] && [ -t 1 ] && pretty="1" +[ -n "$CI" ] && pretty="" + +for option in "${options[@]}"; do + case "$option" in + "h" | "help" ) + help + exit 0 + ;; + "v" | "version" ) + version + exit 0 + ;; + "c" | "count" ) + count_flag="-c" + ;; + "t" | "tap" ) + pretty="" + ;; + "p" | "pretty" ) + pretty="1" + ;; + * ) + usage >&2 + exit 1 + ;; + esac +done + +if [ "${#arguments[@]}" -eq 0 ]; then + usage >&2 + exit 1 +fi + +filenames=() +for filename in "${arguments[@]}"; do + if [ -d "$filename" ]; then + shopt -s nullglob + for suite_filename in "$(expand_path "$filename")"/*.bats; do + filenames["${#filenames[@]}"]="$suite_filename" + done + shopt -u nullglob + else + filenames["${#filenames[@]}"]="$(expand_path "$filename")" + fi +done + +if [ "${#filenames[@]}" -eq 1 ]; then + command="bats-exec-test" +else + command="bats-exec-suite" +fi + +if [ -n "$pretty" ]; then + extended_syntax_flag="-x" + formatter="bats-format-tap-stream" +else + extended_syntax_flag="" + formatter="cat" +fi + +set -o pipefail execfail +exec "$command" $count_flag $extended_syntax_flag "${filenames[@]}" | "$formatter" \ No newline at end of file diff --git a/test/bats/bats-exec-suite b/test/bats/bats-exec-suite new file mode 100755 index 00000000..29ab255d --- /dev/null +++ b/test/bats/bats-exec-suite @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -e + +count_only_flag="" +if [ "$1" = "-c" ]; then + count_only_flag=1 + shift +fi + +extended_syntax_flag="" +if [ "$1" = "-x" ]; then + extended_syntax_flag="-x" + shift +fi + +trap "kill 0; exit 1" int + +count=0 +for filename in "$@"; do + let count+="$(bats-exec-test -c "$filename")" +done + +if [ -n "$count_only_flag" ]; then + echo "$count" + exit +fi + +echo "1..$count" +status=0 +offset=0 +for filename in "$@"; do + index=0 + { + IFS= read -r # 1..n + while IFS= read -r line; do + case "$line" in + "begin "* ) + let index+=1 + echo "${line/ $index / $(($offset + $index)) }" + ;; + "ok "* | "not ok "* ) + [ -n "$extended_syntax_flag" ] || let index+=1 + echo "${line/ $index / $(($offset + $index)) }" + [ "${line:0:6}" != "not ok" ] || status=1 + ;; + * ) + echo "$line" + ;; + esac + done + } < <( bats-exec-test $extended_syntax_flag "$filename" ) + offset=$(($offset + $index)) +done + +exit "$status" diff --git a/test/bats/bats-exec-test b/test/bats/bats-exec-test new file mode 100755 index 00000000..8f3bd510 --- /dev/null +++ b/test/bats/bats-exec-test @@ -0,0 +1,346 @@ +#!/usr/bin/env bash +set -e +set -E +set -T + +BATS_COUNT_ONLY="" +if [ "$1" = "-c" ]; then + BATS_COUNT_ONLY=1 + shift +fi + +BATS_EXTENDED_SYNTAX="" +if [ "$1" = "-x" ]; then + BATS_EXTENDED_SYNTAX="$1" + shift +fi + +BATS_TEST_FILENAME="$1" +if [ -z "$BATS_TEST_FILENAME" ]; then + echo "usage: bats-exec " >&2 + exit 1 +elif [ ! -f "$BATS_TEST_FILENAME" ]; then + echo "bats: $BATS_TEST_FILENAME does not exist" >&2 + exit 1 +else + shift +fi + +BATS_TEST_DIRNAME="$(dirname "$BATS_TEST_FILENAME")" +BATS_TEST_NAMES=() + +load() { + local name="$1" + local filename + + if [ "${name:0:1}" = "/" ]; then + filename="${name}" + else + filename="$BATS_TEST_DIRNAME/${name}.bash" + fi + + [ -f "$filename" ] || { + echo "bats: $filename does not exist" >&2 + exit 1 + } + + source "${filename}" +} + +run() { + local e E T oldIFS + [[ ! "$-" =~ e ]] || e=1 + [[ ! "$-" =~ E ]] || E=1 + [[ ! "$-" =~ T ]] || T=1 + set +e + set +E + set +T + output="$("$@" 2>&1)" + status="$?" + oldIFS=$IFS + IFS=$'\n' lines=($output) + [ -z "$e" ] || set -e + [ -z "$E" ] || set -E + [ -z "$T" ] || set -T + IFS=$oldIFS +} + +setup() { + true +} + +teardown() { + true +} + +skip() { + BATS_TEST_SKIPPED=${1:-1} + BATS_TEST_COMPLETED=1 + exit 0 +} + +bats_test_begin() { + BATS_TEST_DESCRIPTION="$1" + if [ -n "$BATS_EXTENDED_SYNTAX" ]; then + echo "begin $BATS_TEST_NUMBER $BATS_TEST_DESCRIPTION" >&3 + fi + setup +} + +bats_test_function() { + local test_name="$1" + BATS_TEST_NAMES["${#BATS_TEST_NAMES[@]}"]="$test_name" +} + +bats_capture_stack_trace() { + BATS_PREVIOUS_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" ) + BATS_CURRENT_STACK_TRACE=() + + local test_pattern=" $BATS_TEST_NAME $BATS_TEST_SOURCE" + local setup_pattern=" setup $BATS_TEST_SOURCE" + local teardown_pattern=" teardown $BATS_TEST_SOURCE" + + local frame + local index=1 + + while frame="$(caller "$index")"; do + BATS_CURRENT_STACK_TRACE["${#BATS_CURRENT_STACK_TRACE[@]}"]="$frame" + if [[ "$frame" = *"$test_pattern" || \ + "$frame" = *"$setup_pattern" || \ + "$frame" = *"$teardown_pattern" ]]; then + break + else + let index+=1 + fi + done + + BATS_SOURCE="$(bats_frame_filename "${BATS_CURRENT_STACK_TRACE[0]}")" + BATS_LINENO="$(bats_frame_lineno "${BATS_CURRENT_STACK_TRACE[0]}")" +} + +bats_print_stack_trace() { + local frame + local index=1 + local count="${#@}" + + for frame in "$@"; do + local filename="$(bats_trim_filename "$(bats_frame_filename "$frame")")" + local lineno="$(bats_frame_lineno "$frame")" + + if [ $index -eq 1 ]; then + echo -n "# (" + else + echo -n "# " + fi + + local fn="$(bats_frame_function "$frame")" + if [ "$fn" != "$BATS_TEST_NAME" ]; then + echo -n "from function \`$fn' " + fi + + if [ $index -eq $count ]; then + echo "in test file $filename, line $lineno)" + else + echo "in file $filename, line $lineno," + fi + + let index+=1 + done +} + +bats_print_failed_command() { + local frame="$1" + local status="$2" + local filename="$(bats_frame_filename "$frame")" + local lineno="$(bats_frame_lineno "$frame")" + + local failed_line="$(bats_extract_line "$filename" "$lineno")" + local failed_command="$(bats_strip_string "$failed_line")" + echo -n "# \`${failed_command}' " + + if [ $status -eq 1 ]; then + echo "failed" + else + echo "failed with status $status" + fi +} + +bats_frame_lineno() { + local frame="$1" + local lineno="${frame%% *}" + echo "$lineno" +} + +bats_frame_function() { + local frame="$1" + local rest="${frame#* }" + local fn="${rest%% *}" + echo "$fn" +} + +bats_frame_filename() { + local frame="$1" + local rest="${frame#* }" + local filename="${rest#* }" + + if [ "$filename" = "$BATS_TEST_SOURCE" ]; then + echo "$BATS_TEST_FILENAME" + else + echo "$filename" + fi +} + +bats_extract_line() { + local filename="$1" + local lineno="$2" + sed -n "${lineno}p" "$filename" +} + +bats_strip_string() { + local string="$1" + printf "%s" "$string" | sed -e "s/^[ "$'\t'"]*//" -e "s/[ "$'\t'"]*$//" +} + +bats_trim_filename() { + local filename="$1" + local length="${#BATS_CWD}" + + if [ "${filename:0:length+1}" = "${BATS_CWD}/" ]; then + echo "${filename:length+1}" + else + echo "$filename" + fi +} + +bats_debug_trap() { + if [ "$BASH_SOURCE" != "$1" ]; then + bats_capture_stack_trace + fi +} + +bats_error_trap() { + BATS_ERROR_STATUS="$?" + BATS_ERROR_STACK_TRACE=( "${BATS_PREVIOUS_STACK_TRACE[@]}" ) + trap - debug +} + +bats_teardown_trap() { + trap "bats_exit_trap" exit + local status=0 + teardown >>"$BATS_OUT" 2>&1 || status="$?" + + if [ $status -eq 0 ]; then + BATS_TEARDOWN_COMPLETED=1 + elif [ -n "$BATS_TEST_COMPLETED" ]; then + BATS_ERROR_STATUS="$status" + BATS_ERROR_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" ) + fi + + bats_exit_trap +} + +bats_exit_trap() { + local status + local skipped + trap - err exit + + skipped="" + if [ -n "$BATS_TEST_SKIPPED" ]; then + skipped=" # skip" + if [ "1" != "$BATS_TEST_SKIPPED" ]; then + skipped+=" ($BATS_TEST_SKIPPED)" + fi + fi + + if [ -z "$BATS_TEST_COMPLETED" ] || [ -z "$BATS_TEARDOWN_COMPLETED" ]; then + echo "not ok $BATS_TEST_NUMBER $BATS_TEST_DESCRIPTION" >&3 + bats_print_stack_trace "${BATS_ERROR_STACK_TRACE[@]}" >&3 + bats_print_failed_command "${BATS_ERROR_STACK_TRACE[${#BATS_ERROR_STACK_TRACE[@]}-1]}" "$BATS_ERROR_STATUS" >&3 + sed -e "s/^/# /" < "$BATS_OUT" >&3 + status=1 + else + echo "ok ${BATS_TEST_NUMBER}${skipped} ${BATS_TEST_DESCRIPTION}" >&3 + status=0 + fi + + rm -f "$BATS_OUT" + exit "$status" +} + +bats_perform_tests() { + echo "1..$#" + test_number=1 + status=0 + for test_name in "$@"; do + "$0" $BATS_EXTENDED_SYNTAX "$BATS_TEST_FILENAME" "$test_name" "$test_number" || status=1 + let test_number+=1 + done + exit "$status" +} + +bats_perform_test() { + BATS_TEST_NAME="$1" + if [ "$(type -t "$BATS_TEST_NAME" || true)" = "function" ]; then + BATS_TEST_NUMBER="$2" + if [ -z "$BATS_TEST_NUMBER" ]; then + echo "1..1" + BATS_TEST_NUMBER="1" + fi + + BATS_TEST_COMPLETED="" + BATS_TEARDOWN_COMPLETED="" + trap "bats_debug_trap \"\$BASH_SOURCE\"" debug + trap "bats_error_trap" err + trap "bats_teardown_trap" exit + "$BATS_TEST_NAME" >>"$BATS_OUT" 2>&1 + BATS_TEST_COMPLETED=1 + + else + echo "bats: unknown test name \`$BATS_TEST_NAME'" >&2 + exit 1 + fi +} + +if [ -z "$TMPDIR" ]; then + BATS_TMPDIR="/tmp" +else + BATS_TMPDIR="${TMPDIR%/}" +fi + +BATS_TMPNAME="$BATS_TMPDIR/bats.$$" +BATS_PARENT_TMPNAME="$BATS_TMPDIR/bats.$PPID" +BATS_OUT="${BATS_TMPNAME}.out" + +bats_preprocess_source() { + BATS_TEST_SOURCE="${BATS_TMPNAME}.src" + { tr -d '\r' < "$BATS_TEST_FILENAME"; echo; } | bats-preprocess > "$BATS_TEST_SOURCE" + trap "bats_cleanup_preprocessed_source" err exit + trap "bats_cleanup_preprocessed_source; exit 1" int +} + +bats_cleanup_preprocessed_source() { + rm -f "$BATS_TEST_SOURCE" +} + +bats_evaluate_preprocessed_source() { + if [ -z "$BATS_TEST_SOURCE" ]; then + BATS_TEST_SOURCE="${BATS_PARENT_TMPNAME}.src" + fi + source "$BATS_TEST_SOURCE" +} + +exec 3<&1 + +if [ "$#" -eq 0 ]; then + bats_preprocess_source + bats_evaluate_preprocessed_source + + if [ -n "$BATS_COUNT_ONLY" ]; then + echo "${#BATS_TEST_NAMES[@]}" + else + bats_perform_tests "${BATS_TEST_NAMES[@]}" + fi +else + bats_evaluate_preprocessed_source + bats_perform_test "$@" +fi diff --git a/test/bats/bats-format-tap-stream b/test/bats/bats-format-tap-stream new file mode 100755 index 00000000..614768f4 --- /dev/null +++ b/test/bats/bats-format-tap-stream @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +set -e + +# Just stream the TAP output (sans extended syntax) if tput is missing +command -v tput >/dev/null || exec grep -v "^begin " + +header_pattern='[0-9]+\.\.[0-9]+' +IFS= read -r header + +if [[ "$header" =~ $header_pattern ]]; then + count="${header:3}" + index=0 + failures=0 + skipped=0 + name="" + count_column_width=$(( ${#count} * 2 + 2 )) +else + # If the first line isn't a TAP plan, print it and pass the rest through + printf "%s\n" "$header" + exec cat +fi + +update_screen_width() { + screen_width="$(tput cols)" + count_column_left=$(( $screen_width - $count_column_width )) +} + +trap update_screen_width WINCH +update_screen_width + +begin() { + go_to_column 0 + printf_with_truncation $(( $count_column_left - 1 )) " %s" "$name" + clear_to_end_of_line + go_to_column $count_column_left + printf "%${#count}s/${count}" "$index" + go_to_column 1 +} + +pass() { + go_to_column 0 + printf " ✓ %s" "$name" + advance +} + +skip() { + local reason="$1" + [ -z "$reason" ] || reason=": $reason" + go_to_column 0 + printf " - %s (skipped%s)" "$name" "$reason" + advance +} + +fail() { + go_to_column 0 + set_color 1 bold + printf " ✗ %s" "$name" + advance +} + +log() { + set_color 1 + printf " %s\n" "$1" + clear_color +} + +summary() { + printf "\n%d test%s" "$count" "$(plural "$count")" + + printf ", %d failure%s" "$failures" "$(plural "$failures")" + + if [ "$skipped" -gt 0 ]; then + printf ", %d skipped" "$skipped" + fi + + printf "\n" +} + +printf_with_truncation() { + local width="$1" + shift + local string="$(printf "$@")" + + if [ "${#string}" -gt "$width" ]; then + printf "%s..." "${string:0:$(( $width - 4 ))}" + else + printf "%s" "$string" + fi +} + +go_to_column() { + local column="$1" + printf "\x1B[%dG" $(( $column + 1 )) +} + +clear_to_end_of_line() { + printf "\x1B[K" +} + +advance() { + clear_to_end_of_line + echo + clear_color +} + +set_color() { + local color="$1" + local weight="$2" + printf "\x1B[%d;%dm" $(( 30 + $color )) "$( [ "$weight" = "bold" ] && echo 1 || echo 22 )" +} + +clear_color() { + printf "\x1B[0m" +} + +plural() { + [ "$1" -eq 1 ] || echo "s" +} + +_buffer="" + +buffer() { + _buffer="${_buffer}$("$@")" +} + +flush() { + printf "%s" "$_buffer" + _buffer="" +} + +finish() { + flush + printf "\n" +} + +trap finish EXIT + +while IFS= read -r line; do + case "$line" in + "begin "* ) + let index+=1 + name="${line#* $index }" + buffer begin + flush + ;; + "ok "* ) + skip_expr="ok $index # skip (\(([^)]*)\))?" + if [[ "$line" =~ $skip_expr ]]; then + let skipped+=1 + buffer skip "${BASH_REMATCH[2]}" + else + buffer pass + fi + ;; + "not ok "* ) + let failures+=1 + buffer fail + ;; + "# "* ) + buffer log "${line:2}" + ;; + esac +done + +buffer summary diff --git a/test/bats/bats-preprocess b/test/bats/bats-preprocess new file mode 100755 index 00000000..04297ed0 --- /dev/null +++ b/test/bats/bats-preprocess @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -e + +encode_name() { + local name="$1" + local result="test_" + + if [[ ! "$name" =~ [^[:alnum:]\ _-] ]]; then + name="${name//_/-5f}" + name="${name//-/-2d}" + name="${name// /_}" + result+="$name" + else + local length="${#name}" + local char i + + for ((i=0; i: Recipient address rejected: User unknown in virtual mailbox table' /var/log/mail.log" 0 - -# Testing that mail is received for existing alias -assert_raises "docker exec mail grep 'to=, orig_to=' /var/log/mail.log | grep 'status=sent'" 0 - -# Testing that mail is redirected for external alias -assert_raises "docker exec mail grep -- '-> ' /var/log/mail.log" 0 - -# Testing that a SPAM is rejected -assert_raises "docker exec mail grep 'Blocked SPAM' /var/log/mail.log | grep spam@external.tld" 0 - -# Testing that a Virus is rejected -assert_raises "docker exec mail grep 'Blocked INFECTED' /var/log/mail.log | grep virus@external.tld" 0 - -# Testing presence of freshclam CRON -assert "docker exec mail crontab -l" "0 1 * * * /usr/bin/freshclam --quiet" - -# Testing that log don't display errors -assert_raises "docker exec mail grep 'non-null host address bits in' /var/log/mail.log" 1 -assert_raises "docker exec mail grep ': error:' /var/log/mail.log" 1 - -# Testing that pop3 container log don't display errors -assert_raises "docker exec mail_pop3 grep 'non-null host address bits in' /var/log/mail.log" 1 -assert_raises "docker exec mail_pop3 grep ': error:' /var/log/mail.log" 1 - -# Testing Spamssassin config in Amavis -assert_raises "docker exec mail_pop3 grep '\$sa_tag_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 2.0'" 0 -assert_raises "docker exec mail_pop3 grep '\$sa_tag2_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 6.31'" 0 -assert_raises "docker exec mail_pop3 grep '\$sa_kill_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 6.31'" 0 -assert_raises "docker exec mail grep '\$sa_tag_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 1.0'" 0 -assert_raises "docker exec mail grep '\$sa_tag2_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 2.0'" 0 -assert_raises "docker exec mail grep '\$sa_kill_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 3.0'" 0 - -# Testing OpenDKIM -assert "docker exec mail cat /etc/opendkim/KeyTable | wc -l" "2" -assert "docker exec mail ls -l /etc/opendkim/keys/ | grep '^d' | wc -l" "2" - -# Testing OpenDMARC -assert "docker exec mail cat /etc/opendmarc.conf | grep ^AuthservID | wc -l" "1" -assert "docker exec mail cat /etc/opendmarc.conf | grep ^TrustedAuthservID | wc -l" "1" - -# Testing hostname config -assert "docker exec mail cat /etc/mailname" "my-domain.com" - -# Testing presence of LetsEncrypt signed certs -assert_raises "docker exec mail grep 'BEGIN CERTIFICATE' /etc/ssl/certs/lets-encrypt-x1-cross-signed.pem" "0" -assert_raises "docker exec mail grep 'BEGIN CERTIFICATE' /etc/ssl/certs/lets-encrypt-x2-cross-signed.pem" "0" - -# Testing generated ssl certs -assert_raises "docker exec mail openssl s_client -connect 0.0.0.0:587 -starttls smtp -CApath /etc/ssl/certs/ | grep 'Verify return code: 0 (ok)'" "0" - -# Testing fail2ban -assert_raises "docker exec mail fail2ban-client status sasl | grep 'IP list:\s*127.0.0.1'" 1 - -docker exec mail fail2ban-client set sasl delignoreip 127.0.0.1/8 &> /dev/null - -docker exec mail /bin/sh -c 'nc -w 1 0.0.0.0 25 < /tmp/test/auth/smtp-auth-login-wrong.txt' &> /dev/null -docker exec mail /bin/sh -c 'nc -w 1 0.0.0.0 25 < /tmp/test/auth/smtp-auth-login-wrong.txt' &> /dev/null -docker exec mail /bin/sh -c 'nc -w 1 0.0.0.0 25 < /tmp/test/auth/smtp-auth-login-wrong.txt' &> /dev/null - -sleep 10 -assert_raises "docker exec mail fail2ban-client status sasl | grep 'IP list:\s*127.0.0.1'" 0 - -docker exec mail fail2ban-client set sasl addignoreip 127.0.0.1/8 &> /dev/null -docker exec mail fail2ban-client set sasl unbanip 127.0.0.1 &> /dev/null - -sleep 10 -assert_raises "docker exec mail fail2ban-client status sasl | grep 'IP list:\s*127.0.0.1'" 1 - -# Ending tests -assert_end diff --git a/test/tests.bats b/test/tests.bats new file mode 100644 index 00000000..7eeed0ce --- /dev/null +++ b/test/tests.bats @@ -0,0 +1,340 @@ +# +# processes +# + +@test "checking process: postfix" { + run docker exec mail /bin/bash -c "ps aux --forest | grep -v grep | grep '/usr/lib/postfix/master'" + [ "$status" -eq 0 ] +} + +@test "checking process: saslauthd" { + run docker exec mail /bin/bash -c "ps aux --forest | grep -v grep | grep '/usr/sbin/saslauthd'" + [ "$status" -eq 0 ] +} + +@test "checking process: clamd" { + run docker exec mail /bin/bash -c "ps aux --forest | grep -v grep | grep '/usr/sbin/clamd'" + [ "$status" -eq 0 ] +} + +@test "checking process: new" { + run docker exec mail /bin/bash -c "ps aux --forest | grep -v grep | grep '/usr/sbin/amavisd-new'" + [ "$status" -eq 0 ] +} + +@test "checking process: opendkim" { + run docker exec mail /bin/bash -c "ps aux --forest | grep -v grep | grep '/usr/sbin/opendkim'" + [ "$status" -eq 0 ] +} + +@test "checking process: opendmarc" { + run docker exec mail /bin/bash -c "ps aux --forest | grep -v grep | grep '/usr/sbin/opendmarc'" + [ "$status" -eq 0 ] +} + +@test "checking process: fail2ban" { + run docker exec mail /bin/bash -c "ps aux --forest | grep -v grep | grep '/usr/bin/python /usr/bin/fail2ban-server'" + [ "$status" -eq 0 ] +} + +@test "checking process: courierpop3d (disabled in default configuration)" { + run docker exec mail /bin/bash -c "ps aux --forest | grep -v grep | grep '/usr/lib/courier/courier/courierpop3d'" + [ "$status" -eq 1 ] +} + +@test "checking process: courierpop3d (pop3 server enabled)" { + run docker exec mail_pop3 /bin/bash -c "ps aux --forest | grep -v grep | grep '/usr/lib/courier/courier/courierpop3d'" + [ "$status" -eq 0 ] +} + +# +# imap +# + +@test "checking imap: server is ready with STARTTLS" { + run docker exec mail /bin/bash -c "nc -w 1 0.0.0.0 143 | grep '* OK' | grep 'STARTTLS' | grep 'Courier-IMAP ready'" + [ "$status" -eq 0 ] +} + +@test "checking imap: authentication works" { + run docker exec mail /bin/sh -c "nc -w 1 0.0.0.0 143 < /tmp/test/auth/imap-auth.txt" + [ "$status" -eq 0 ] +} + +# +# pop +# + +@test "checking pop: server is ready" { + run docker exec mail_pop3 /bin/bash -c "nc -w 1 0.0.0.0 110 | grep '+OK'" + [ "$status" -eq 0 ] +} + +@test "checking pop: authentication works" { + run docker exec mail_pop3 /bin/sh -c "nc -w 1 0.0.0.0 110 < /tmp/test/auth/pop3-auth.txt" + [ "$status" -eq 0 ] +} + +# +# sasl +# + +@test "checking sasl: testsaslauthd works with good password" { + run docker exec mail /bin/sh -c "testsaslauthd -u user2 -r otherdomain.tld -p mypassword | grep 'OK \"Success.\"'" + [ "$status" -eq 0 ] +} + +@test "checking sasl: testsaslauthd fails with bad password" { + run docker exec mail /bin/sh -c "testsaslauthd -u user2 -r otherdomain.tld -p BADPASSWORD | grep 'NO \"authentication failed\"'" + [ "$status" -eq 0 ] +} + +# +# smtp +# + +@test "checking smtp: authentication works with good password (plain)" { + run docker exec mail /bin/sh -c "nc -w 1 0.0.0.0 25 < /tmp/test/auth/smtp-auth-plain.txt | grep 'Authentication successful'" + [ "$status" -eq 0 ] +} + +@test "checking smtp: authentication fails with wrong password (plain)" { + run docker exec mail /bin/sh -c "nc -w 1 0.0.0.0 25 < /tmp/test/auth/smtp-auth-plain-wrong.txt | grep 'authentication failed'" + [ "$status" -eq 0 ] +} + +@test "checking smtp: authentication works with good password (login)" { + run docker exec mail /bin/sh -c "nc -w 1 0.0.0.0 25 < /tmp/test/auth/smtp-auth-login.txt | grep 'Authentication successful'" + [ "$status" -eq 0 ] +} + +@test "checking smtp: authentication fails with wrong password (login)" { + run docker exec mail /bin/sh -c "nc -w 1 0.0.0.0 25 < /tmp/test/auth/smtp-auth-login-wrong.txt | grep 'authentication failed'" + [ "$status" -eq 0 ] +} + +@test "checking smtp: delivers mail to existing account" { + run docker exec mail /bin/sh -c "grep 'status=sent (delivered to maildir)' /var/log/mail.log | wc -l" + [ "$status" -eq 0 ] + [ "$output" -eq 2 ] +} + +@test "checking smtp: delivers mail to existing alias" { + run docker exec mail /bin/sh -c "grep 'to=, orig_to=' /var/log/mail.log | grep 'status=sent' | wc -l" + [ "$status" -eq 0 ] + [ "$output" = 1 ] +} + +@test "checking smtp: user1 should have received 2 mails" { + run docker exec mail /bin/sh -c "ls -A /var/mail/localhost.localdomain/user1/new | wc -l" + [ "$status" -eq 0 ] + [ "$output" = 2 ] +} + +@test "checking smtp: rejects mail to unknown user" { + run docker exec mail /bin/sh -c "grep ': Recipient address rejected: User unknown in virtual mailbox table' /var/log/mail.log | wc -l" + [ "$status" -eq 0 ] + [ "$output" = 1 ] +} + +@test "checking smtp: redirects mail to external alias" { + run docker exec mail /bin/sh -c "grep -- '-> ' /var/log/mail.log | wc -l" + [ "$status" -eq 0 ] + [ "$output" = 1 ] +} + +@test "checking smtp: rejects spam" { + run docker exec mail /bin/sh -c "grep 'Blocked SPAM' /var/log/mail.log | grep spam@external.tld | wc -l" + [ "$status" -eq 0 ] + [ "$output" = 1 ] +} + +@test "checking smtp: rejects virus" { + run docker exec mail /bin/sh -c "grep 'Blocked INFECTED' /var/log/mail.log | grep virus@external.tld | wc -l" + [ "$status" -eq 0 ] + [ "$output" = 1 ] +} + +# +# accounts +# + +@test "checking accounts: user accounts" { + run docker exec mail sasldblistusers2 + [ "$status" -eq 0 ] + [ "${lines[0]}" = "user1@localhost.localdomain: userPassword" ] + [ "${lines[1]}" = "user2@otherdomain.tld: userPassword" ] +} + +@test "checking accounts: user mail folders for user1" { + run docker exec mail ls -A /var/mail/localhost.localdomain/user1 + [ "$status" -eq 0 ] + [ "${lines[0]}" = ".Drafts" ] + [ "${lines[1]}" = ".Sent" ] + [ "${lines[2]}" = ".Trash" ] + [ "${lines[3]}" = "courierimapsubscribed" ] + [ "${lines[4]}" = "cur" ] + [ "${lines[5]}" = "new" ] + [ "${lines[6]}" = "tmp" ] +} + +@test "checking accounts: user mail folders for user2" { + run docker exec mail ls -A /var/mail/otherdomain.tld/user2 + [ "$status" -eq 0 ] + [ "${lines[0]}" = ".Drafts" ] + [ "${lines[1]}" = ".Sent" ] + [ "${lines[2]}" = ".Trash" ] + [ "${lines[3]}" = "courierimapsubscribed" ] + [ "${lines[4]}" = "cur" ] + [ "${lines[5]}" = "new" ] + [ "${lines[6]}" = "tmp" ] +} + +# +# postfix +# + +@test "checking postfix: vhost file is correct" { + run docker exec mail cat /etc/postfix/vhost + [ "$status" -eq 0 ] + [ "${lines[0]}" = "localhost.localdomain" ] + [ "${lines[1]}" = "otherdomain.tld" ] +} + +# +# spamassassin +# + +@test "checking spamassassin: docker env variables are set correctly (default)" { + run docker exec mail_pop3 /bin/sh -c "grep '\$sa_tag_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 2.0'" + [ "$status" -eq 0 ] + run docker exec mail_pop3 /bin/sh -c "grep '\$sa_tag2_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 6.31'" + [ "$status" -eq 0 ] + run docker exec mail_pop3 /bin/sh -c "grep '\$sa_kill_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 6.31'" + [ "$status" -eq 0 ] +} + +@test "checking spamassassin: docker env variables are set correctly (custom)" { + run docker exec mail /bin/sh -c "grep '\$sa_tag_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 1.0'" + [ "$status" -eq 0 ] + run docker exec mail /bin/sh -c "grep '\$sa_tag2_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 2.0'" + [ "$status" -eq 0 ] + run docker exec mail /bin/sh -c "grep '\$sa_kill_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 3.0'" + [ "$status" -eq 0 ] +} + +# +# opendkim +# + +@test "checking opendkim: /etc/opendkim/KeyTable should contain 2 entries" { + run docker exec mail /bin/sh -c "cat /etc/opendkim/KeyTable | wc -l" + [ "$status" -eq 0 ] + [ "$output" -eq 2 ] +} + +@test "checking opendkim: /etc/opendkim/keys/ should contain 2 entries" { + run docker exec mail /bin/sh -c "ls -l /etc/opendkim/keys/ | grep '^d' | wc -l" + [ "$status" -eq 0 ] + [ "$output" -eq 2 ] +} + +# +# opendmarc +# + +@test "checking opendkim: server fqdn should be added to /etc/opendmarc.conf as AuthservID" { + run docker exec mail grep ^AuthservID /etc/opendmarc.conf + [ "$status" -eq 0 ] + [ "$output" = "AuthservID mail.my-domain.com" ] +} + +@test "checking opendkim: server fqdn should be added to /etc/opendmarc.conf as TrustedAuthservIDs" { + run docker exec mail grep ^TrustedAuthservID /etc/opendmarc.conf + [ "$status" -eq 0 ] + [ "$output" = "TrustedAuthservIDs mail.my-domain.com" ] +} + +# +# letsencrypt +# + +@test "checking letsencrypt: lets-encrypt-x1-cross-signed.pem is installed" { + run docker exec mail grep 'BEGIN CERTIFICATE' /etc/ssl/certs/lets-encrypt-x1-cross-signed.pem + [ "$status" -eq 0 ] +} + +@test "checking letsencrypt: lets-encrypt-x2-cross-signed.pem is installed" { + run docker exec mail grep 'BEGIN CERTIFICATE' /etc/ssl/certs/lets-encrypt-x2-cross-signed.pem + [ "$status" -eq 0 ] +} + +# +# ssl +# + +@test "checking ssl: generated default cert is installed" { + run docker exec mail /bin/sh -c "openssl s_client -connect 0.0.0.0:587 -starttls smtp -CApath /etc/ssl/certs/ | grep 'Verify return code: 0 (ok)'" + [ "$status" -eq 0 ] +} + +# +# fail2ban +# + +@test "checking fail2ban: localhost is not banned" { + run docker exec mail /bin/sh -c "fail2ban-client status sasl | grep 'IP list:\s*127.0.0.1'" + [ "$status" -eq 1 ] +} + +@test "checking fail2ban: ban ip on multiple failed login" { + docker exec mail fail2ban-client status sasl + docker exec mail fail2ban-client set sasl delignoreip 127.0.0.1/8 + docker exec mail /bin/sh -c 'nc -w 1 0.0.0.0 25 < /tmp/test/auth/smtp-auth-login-wrong.txt' + docker exec mail /bin/sh -c 'nc -w 1 0.0.0.0 25 < /tmp/test/auth/smtp-auth-login-wrong.txt' + docker exec mail /bin/sh -c 'nc -w 1 0.0.0.0 25 < /tmp/test/auth/smtp-auth-login-wrong.txt' + sleep 5 + run docker exec mail /bin/sh -c "fail2ban-client status sasl | grep 'IP list:\s*127.0.0.1'" + [ "$status" -eq 0 ] +} + +@test "checking fail2ban: unban ip works" { + docker exec mail fail2ban-client set sasl addignoreip 127.0.0.1/8 + docker exec mail fail2ban-client set sasl unbanip 127.0.0.1 + sleep 5 + run docker exec mail /bin/sh -c "fail2ban-client status sasl | grep 'IP list:\s*127.0.0.1'" + [ "$status" -eq 1 ] +} + +# +# system +# + +@test "checking system: freshclam cron is enabled" { + run docker exec mail crontab -l + [ "$status" -eq 0 ] + [ "$output" = "0 1 * * * /usr/bin/freshclam --quiet" ] +} + +@test "checking system: /var/log/mail.log is error free" { + run docker exec mail grep 'non-null host address bits in' /var/log/mail.log + [ "$status" -eq 1 ] + run docker exec mail grep ': error:' /var/log/mail.log + [ "$status" -eq 1 ] + run docker exec mail_pop3 grep 'non-null host address bits in' /var/log/mail.log + [ "$status" -eq 1 ] + run docker exec mail_pop3 grep ': error:' /var/log/mail.log + [ "$status" -eq 1 ] +} + +@test "checking system: sets the server fqdn" { + run docker exec mail hostname + [ "$status" -eq 0 ] + [ "$output" = "mail.my-domain.com" ] +} + +@test "checking system: sets the server domain name in /etc/mailname" { + run docker exec mail cat /etc/mailname + [ "$status" -eq 0 ] + [ "$output" = "my-domain.com" ] +}