diff --git a/.dockerignore b/.dockerignore index 14e0205..31577a6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,5 @@ .git dkim.key +Dockerfile* +test/ +Makefile diff --git a/Dockerfile b/Dockerfile index 3e17402..08f9eff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,10 +14,8 @@ RUN set -x \ && go install -tags nosystemd,nodocker \ ; -# Postfix SMTP Relay - # Debian Trixie -FROM debian:13 +FROM debian:13 AS base EXPOSE 25 587 2525 @@ -61,5 +59,19 @@ RUN set -x \ && chmod 0644 /etc/postfix/header_checks \ ; +# Development image +FROM base AS development + +# Install packages +RUN set -x \ +&& export DEBIAN_FRONTEND=noninteractive \ +&& apt-get update \ +&& apt-get install -y --no-install-recommends bats \ +&& apt-get clean \ +&& rm -rf /var/lib/apt/lists/* \ +; + +# Postfix SMTP Relay +FROM base AS build ENTRYPOINT ["/entry.sh"] CMD ["/usr/bin/s6-svscan", "/etc/s6"] diff --git a/Makefile b/Makefile index 604a2c6..b61fe81 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ NAME := postfix TAG := latest IMAGE_NAME := panubo/$(NAME) -.PHONY: help bash run run-* build push clean _ci_test +.PHONY: help bash run run-* build push clean test _ci_test help: @printf "$$(grep -hE '^\S+:.*##' $(MAKEFILE_LIST) | sed -e 's/:.*##\s*/:/' -e 's/^\(.\+\):\(.*\)/\\x1b[36m\1\\x1b[m:\2/' | column -c2 -t -s :)\n" @@ -55,7 +55,11 @@ push: ## Pushes the docker image to hub.docker.com clean: ## Remove built image docker rmi $(IMAGE_NAME):$(TAG) -_ci_test: +test: ## Build a test image and run bats tests in docker + docker build --target development -t $(IMAGE_NAME):test-dev . + docker run --rm -v $(shell pwd):/app -w /app $(IMAGE_NAME):test-dev bats test/ + +_ci_test: test true dkim.key: diff --git a/README.md b/README.md index 4f6e823..967eb56 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,9 @@ echo -e "To: Bob \nFrom: Bill \nSubject: Test See the `Makefile` for make targets. +To run the BATS tests, use the `make test` command. This will build a test Docker image and execute the tests within it. + + ## Releases For production usage, please use a versioned release rather than the floating 'latest' tag. diff --git a/etc/opendkim.conf.sh b/etc/opendkim.conf.sh index 019127c..96e43f9 100755 --- a/etc/opendkim.conf.sh +++ b/etc/opendkim.conf.sh @@ -2,10 +2,10 @@ set -e -OUTPUT='/etc/opendkim.conf' +: ${DKIM_CONF:='/etc/opendkim.conf'} # exit if config already exists -[ -f "${OUTPUT}" ] && exit 0 +[ -f "${DKIM_CONF}" ] && exit 0 # defaults : ${DKIM_KEYFILE:='/etc/opendkim/dkim.key'} @@ -40,7 +40,7 @@ echo "dkim >> Setting DKIM_DOMAINS to $DKIM_DOMAINS" echo "dkim >> Setting DKIM_SELECTOR to $DKIM_SELECTOR" # Render the dkim config -cat > ${OUTPUT} < "${DKIM_CONF}" <> Checking Postfix Configuration" -postfix check +postfix check || { echo "postfix >> Error: check failed, dumping config"; cat /etc/postfix/main.cf; exit 1; } echo "postfix >> Starting postfix" exec /usr/sbin/postfix start-fg diff --git a/test/opendkim.bats b/test/opendkim.bats new file mode 100644 index 0000000..d9c51fb --- /dev/null +++ b/test/opendkim.bats @@ -0,0 +1,67 @@ +#!/usr/bin/env bats + +# This setup function runs before each test. +setup() { + # Create a temporary directory for our test files. + BATS_TMPDIR=$(mktemp -d -t bats-XXXXXXXX) + export BATS_TMPDIR + + # Mock the s6-svscanctl command so the script doesn't fail if it's not installed. + # We create a fake executable that does nothing and returns success. + ln -s /usr/bin/true "${BATS_TMPDIR}/s6-svscanctl" + export PATH="${BATS_TMPDIR}:${PATH}" +} + +# This teardown function runs after each test to clean up. +teardown() { + rm -rf "${BATS_TMPDIR}" +} + +@test "should generate config with default values" { + # Define test-specific variables + local output_file="${BATS_TMPDIR}/opendkim.conf" + local dkim_keyfile="${BATS_TMPDIR}/dkim.key" + touch "${dkim_keyfile}" # Create the dummy key file + + # Run the script in a subshell with a clean environment and execution tracing (-x) + ( + export MAILNAME="example.com" + export MYNETWORKS="127.0.0.1" + export DKIM_CONF="${output_file}" + export DKIM_KEYFILE="${dkim_keyfile}" + bash -x etc/opendkim.conf.sh + ) + + # Check that the generated file contains the expected lines. + run grep "Domain example.com" "${output_file}" + [ "$status" -eq 0 ] # Succeeds if grep finds the line + + run grep "Selector mail" "${output_file}" + [ "$status" -eq 0 ] + + run grep "InternalHosts 127.0.0.1" "${output_file}" + [ "$status" -eq 0 ] +} + +@test "should use DKIM_DOMAINS and DKIM_SELECTOR from environment" { + # Define test-specific variables + local output_file="${BATS_TMPDIR}/opendkim.conf" + local dkim_keyfile="${BATS_TMPDIR}/dkim.key" + touch "${dkim_keyfile}" + + # Run the script in a subshell with a clean environment and execution tracing (-x) + ( + export DKIM_DOMAINS="custom.net,another.org" + export DKIM_SELECTOR="dkim2024" + export DKIM_CONF="${output_file}" + export DKIM_KEYFILE="${dkim_keyfile}" + bash -x etc/opendkim.conf.sh + ) + + # Check for the custom values. + run grep "Domain custom.net,another.org" "${output_file}" + [ "$status" -eq 0 ] + + run grep "Selector dkim2024" "${output_file}" + [ "$status" -eq 0 ] +} diff --git a/test/postfix.bats b/test/postfix.bats new file mode 100644 index 0000000..3936c85 --- /dev/null +++ b/test/postfix.bats @@ -0,0 +1,91 @@ +#!/usr/bin/env bats + +# Test for /etc/s6/postfix/run + +setup() { + BATS_TMPDIR=$(mktemp -d -t bats-XXXXXXXX) + export BATS_TMPDIR + + # Backup original files that the script might modify + [ -f /etc/postfix/main.cf ] && cp /etc/postfix/main.cf "${BATS_TMPDIR}/main.cf.bak" + [ -f /etc/sasldb2 ] && cp /etc/sasldb2 "${BATS_TMPDIR}/sasldb2.bak" + cp /usr/lib/postfix/configure-instance.sh "${BATS_TMPDIR}/configure-instance.sh.bak" + + # Create a version of the run script without the final 'exec' to prevent blocking + sed 's/exec \/usr\/sbin\/postfix start-fg//' /etc/s6/postfix/run > "${BATS_TMPDIR}/run" + chmod +x "${BATS_TMPDIR}/run" + + # The script might create these files, so ensure the directory exists + mkdir -p /etc/postfix/sasl +} + +teardown() { + # Restore original files + [ -f "${BATS_TMPDIR}/main.cf.bak" ] && mv "${BATS_TMPDIR}/main.cf.bak" /etc/postfix/main.cf + [ -f "${BATS_TMPDIR}/sasldb2.bak" ] && mv "${BATS_TMPDIR}/sasldb2.bak" /etc/sasldb2 + mv "${BATS_TMPDIR}/configure-instance.sh.bak" /usr/lib/postfix/configure-instance.sh + rm -rf "${BATS_TMPDIR}" +} + +@test "should configure main.cf with custom values from environment variables" { + # Run the modified script in a subshell with custom environment variables + ( + # Set env vars to test against + export MAILNAME="test.example.com" + export MYNETWORKS="10.0.0.0/8 127.0.0.0/8" + export SIZELIMIT="20480000" + export RELAYHOST="[mail.example.com]:587" + export POSTFIX_ADD_MISSING_HEADERS="yes" + export INET_PROTOCOLS="ipv4" + export DISABLE_VRFY_COMMAND="no" + export USE_TLS="no" # Disable TLS to avoid slow cert generation + + # Execute the setup script + bash -x "${BATS_TMPDIR}/run" + ) + + # Use postconf to verify the settings in /etc/postfix/main.cf + run postconf -h myhostname + [ "$status" -eq 0 ] + [ "$output" = "test.example.com" ] + + run postconf -h mynetworks + [ "$status" -eq 0 ] + [ "$output" = "10.0.0.0/8 127.0.0.0/8" ] + + run postconf -h message_size_limit + [ "$status" -eq 0 ] + [ "$output" = "20480000" ] + + run postconf -h relayhost + [ "$status" -eq 0 ] + [ "$output" = "[mail.example.com]:587" ] + + run postconf -h always_add_missing_headers + [ "$status" -eq 0 ] + [ "$output" = "yes" ] + + run postconf -h inet_protocols + [ "$status" -eq 0 ] + [ "$output" = "ipv4" ] + + run postconf -h disable_vrfy_command + [ "$status" -eq 0 ] + [ "$output" = "no" ] +} + +@test "should enable DKIM milter when USE_DKIM is 'yes'" { + ( + export MAILNAME="test.example.com" + export USE_DKIM="yes" + bash -x "${BATS_TMPDIR}/run" + ) + + run postconf -h smtpd_milters + [ "$status" -eq 0 ] + [ "$output" = "inet:localhost:8891" ] + + run postconf -h non_smtpd_milters + [ "$status" -eq 0 ] + [ "$output" = "inet:localhost:8891" ] +}