diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 9765be8..ef275ba 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -17,11 +17,11 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 with: # list of Docker images to use as base name for tags images: | @@ -32,26 +32,26 @@ jobs: org.opencontainers.image.description=MailHog mail testing, configured for use with the pygmy stack - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: . platforms: linux/amd64,linux/arm64 @@ -61,10 +61,10 @@ jobs: test: needs: docker - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # Establish some SSH keys. - name: Setup SSH @@ -77,33 +77,53 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 with: # list of Docker images to use as base name for tags images: | ghcr.io/${{ github.repository_owner }}/mailhog flavor: | latest=false + - + name: Set single image tag + id: single_tag + run: | + echo "tag=$(echo '${{ steps.meta.outputs.tags }}' | head -n1)" >> "$GITHUB_OUTPUT" - name: Find and Replace - uses: jacobtomlinson/gha-find-replace@v3 - with: - find: "ghcr.io/pygmystack/mailhog:main" - replace: ${{ steps.meta.outputs.tags }} - include: "examples/**" - - - name: Show changes + env: + IMAGE_TAG: ${{ steps.single_tag.outputs.tag }} run: | - grep -n ghcr examples/* + find examples/ -type f -exec sed -i.bak "s|ghcr.io/pygmystack/mailhog:main|${IMAGE_TAG}|g" {} \; + find examples/ -name "*.bak" -delete + grep -Fn ghcr examples/* - - name: Install pygmy and dockerize via brew + name: Set up Homebrew + uses: Homebrew/actions/setup-homebrew@ebe7cd12aeb3c69ccf1a6ba5a4eba5bfdfb00b3b # main + - + name: Install homebrew packages + env: + HOMEBREW_NO_AUTO_UPDATE: 1 + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 + HOMEBREW_NO_ENV_HINTS: 1 run: | - eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"; - brew tap pygmystack/pygmy; - brew install pygmy; + brew install bats-core; brew install dockerize; - echo "/home/linuxbrew/.linuxbrew/bin" >> $GITHUB_PATH; + brew install pygmystack/pygmy/pygmy; pygmy version; + - + name: Pull image for tests + run: | + docker pull ${{ steps.single_tag.outputs.tag }} + docker pull uselagoon/php-8.5-fpm + - + name: Run BATS tests + env: + IMAGE_NAME: ${{ steps.single_tag.outputs.tag }} + PHP_FPM_IMAGE: uselagoon/php-8.5-fpm + run: | + bats --tap tests/image_structure.bats + bats --tap tests/runtime.bats - name: Switch pygmy configs from vanilla to basic run: | @@ -123,7 +143,7 @@ jobs: pygmy --config examples/pygmy.basic.yml export -o ./exported-config.yml cat ./exported-config.yml echo "Checking image references in started containers..."; - docker container inspect amazeeio-mailhog | jq '.[].Config.Image' | grep '${{ steps.meta.outputs.tags }}'; + docker container inspect amazeeio-mailhog | jq '.[].Config.Image' | grep -F '${{ steps.single_tag.outputs.tag }}'; - name: Resolv file test run: | @@ -136,9 +156,9 @@ jobs: curl http://docker.amazee.io/stats | grep 'class=px' | grep 'mailhog.docker.amazee.io'; curl http://docker.amazee.io/stats | grep 'HAProxy version'; - - name: mailhog test + name: mailpit test run: | - curl http://mailhog.docker.amazee.io | grep 'mailhog/MailHog'; + curl http://mailhog.docker.amazee.io | grep 'Mailpit'; - name: dnsmasq version run: | @@ -185,7 +205,7 @@ jobs: pygmy --config examples/pygmy.yml export -o ./exported-config-2.yml cat ./exported-config-2.yml echo "Checking image references in started containers..."; - docker container inspect amazeeio-mailhog | jq '.[].Config.Image' | grep '${{ steps.meta.outputs.tags }}'; + docker container inspect amazeeio-mailhog | jq '.[].Config.Image' | grep -F '${{ steps.single_tag.outputs.tag }}'; - name: SSH Key test run: | @@ -201,14 +221,14 @@ jobs: name: "[Example] Drupal Base" run: | cd lagoon-examples/drupal-base; - docker-compose -p drupal-base up -d; - docker-compose -p drupal-base exec -T cli composer install; + docker compose -p drupal-base up -d; + docker compose -p drupal-base exec -T cli composer install; dockerize -wait http://drupal-base.docker.amazee.io:80 -timeout 10s; curl --HEAD http://drupal-base.docker.amazee.io; curl --HEAD http://drupal-base.docker.amazee.io | grep -i "x-lagoon"; pygmy --config examples/pygmy.yml status | grep '\- http://drupal-base.docker.amazee.io'; - docker-compose -p drupal-base down; - docker-compose -p drupal-base rm; + docker compose -p drupal-base down; + docker compose -p drupal-base rm; cd ../../; - name: Test the stop command diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8425769 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "chat.tools.terminal.autoApprove": { + "bats": true + } +} diff --git a/Dockerfile b/Dockerfile index 5c253be..ce683ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,7 @@ -FROM golang:1.21-alpine3.19 as builder +FROM axllent/mailpit:v1.29 -RUN apk --no-cache add --virtual \ - build-dependencies \ - git \ - && GOPATH=/tmp/gocode go install github.com/mailhog/MailHog@v1.0.1 +ENV MP_UI_BIND_ADDR=[::]:80 -FROM alpine:3.19 -WORKDIR /bin -COPY --from=builder tmp/gocode/bin/MailHog /bin/MailHog -EXPOSE 1025 8025 -ENTRYPOINT ["MailHog"] +RUN ln -s /mailpit /bin/MailHog + +ENTRYPOINT ["/mailpit"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c41cc9c --- /dev/null +++ b/Makefile @@ -0,0 +1,52 @@ +PHP_FPM_IMAGE ?= uselagoon/php-8.5-fpm +IMAGE_NAME ?= pygmystack/mailhog +IMAGE_TAG ?= test +FULL_IMAGE := $(IMAGE_NAME):$(IMAGE_TAG) + +.DEFAULT_GOAL := help + +.PHONY: help build test test-bats test-structure test-runtime shell clean + +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ + awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}' + +build: ## Build the Docker image + docker build --tag $(FULL_IMAGE) . + +test: build ## Build the image and run all BATS tests (requires: brew install bats-core) + @command -v bats >/dev/null 2>&1 || { \ + echo "Error: bats is not installed. Install with: brew install bats-core"; \ + exit 1; \ + } + docker pull $(PHP_FPM_IMAGE) + IMAGE_NAME=$(FULL_IMAGE) PHP_FPM_IMAGE=$(PHP_FPM_IMAGE) bats --tap tests/ + +test-bats: ## Run all BATS tests without rebuilding the image + @command -v bats >/dev/null 2>&1 || { \ + echo "Error: bats is not installed. Install with: brew install bats-core"; \ + exit 1; \ + } + docker pull $(PHP_FPM_IMAGE) + IMAGE_NAME=$(FULL_IMAGE) PHP_FPM_IMAGE=$(PHP_FPM_IMAGE) bats --tap tests/ + +test-structure: ## Run image structure tests only (no running container required) + @command -v bats >/dev/null 2>&1 || { \ + echo "Error: bats is not installed. Install with: brew install bats-core"; \ + exit 1; \ + } + IMAGE_NAME=$(FULL_IMAGE) bats --tap tests/image_structure.bats + +test-runtime: ## Run runtime and email-flow tests (starts a MailHog container) + @command -v bats >/dev/null 2>&1 || { \ + echo "Error: bats is not installed. Install with: brew install bats-core"; \ + exit 1; \ + } + docker pull $(PHP_FPM_IMAGE) + IMAGE_NAME=$(FULL_IMAGE) PHP_FPM_IMAGE=$(PHP_FPM_IMAGE) bats --tap tests/runtime.bats + +shell: ## Open an interactive shell inside the container + docker run --rm -it --entrypoint sh $(FULL_IMAGE) + +clean: ## Remove the local test Docker image + docker rmi $(FULL_IMAGE) 2>/dev/null || true diff --git a/tests/image_structure.bats b/tests/image_structure.bats new file mode 100644 index 0000000..d3c1469 --- /dev/null +++ b/tests/image_structure.bats @@ -0,0 +1,63 @@ +#!/usr/bin/env bats +# Image structure tests — verify the binary, exposed ports, entrypoint, and +# working directory baked into the Mailpit image. Tests run ephemeral +# containers and do not require a long-running process. + +bats_require_minimum_version 1.5.0 + +IMAGE="${IMAGE_NAME:-pygmystack/mailhog:test}" + +# --------------------------------------------------------------------------- +# Binary +# --------------------------------------------------------------------------- + +@test "mailpit binary is present at /mailpit" { + run docker run --rm --entrypoint sh "${IMAGE}" -c 'test -f /mailpit' + [ "$status" -eq 0 ] +} + +@test "mailpit binary is executable" { + run docker run --rm --entrypoint sh "${IMAGE}" -c 'test -x /mailpit' + [ "$status" -eq 0 ] +} + +@test "/bin/MailHog symlink points to /mailpit" { + run docker run --rm --entrypoint sh "${IMAGE}" -c 'test -L /bin/MailHog' + [ "$status" -eq 0 ] +} + +# --------------------------------------------------------------------------- +# Exposed ports (image metadata) +# --------------------------------------------------------------------------- + +@test "image declares SMTP port 1025 as exposed" { + run docker inspect --format='{{json .Config.ExposedPorts}}' "${IMAGE}" + [ "$status" -eq 0 ] + [[ "$output" =~ "1025" ]] +} + +@test "image declares HTTP port 80 as exposed" { + run docker inspect --format='{{json .Config.ExposedPorts}}' "${IMAGE}" + [ "$status" -eq 0 ] + [[ "$output" =~ "\"80" ]] +} + +# --------------------------------------------------------------------------- +# Entrypoint +# --------------------------------------------------------------------------- + +@test "image entrypoint is mailpit" { + run docker inspect --format='{{json .Config.Entrypoint}}' "${IMAGE}" + [ "$status" -eq 0 ] + [[ "$output" =~ "mailpit" ]] +} + +# --------------------------------------------------------------------------- +# Working directory +# --------------------------------------------------------------------------- + +@test "image working directory is /" { + run docker inspect --format='{{.Config.WorkingDir}}' "${IMAGE}" + [ "$status" -eq 0 ] + [ "$output" = "/" ] +} diff --git a/tests/runtime.bats b/tests/runtime.bats new file mode 100644 index 0000000..57eebd9 --- /dev/null +++ b/tests/runtime.bats @@ -0,0 +1,436 @@ +#!/usr/bin/env bats +# Runtime tests — start a Mailpit container and exercise SMTP reception and +# the HTTP API. +# +# A dedicated container is started once in setup_file() and torn down in +# teardown_file(). The container is placed on a private Docker network so +# that Lagoon-style PHP container tests can reach Mailpit via the well-known +# hostname "amazeeio-mailpit" (matching the alias used by Lagoon's 50-ssmtp.sh +# entrypoint). Host-mapped ports are used for curl-based SMTP sends and +# API assertions so that the tests work on both Linux and macOS (Docker +# Desktop). +# +# Tests are ordered intentionally: the empty-mailbox assertions run before +# any email-send tests; deletion tests run last. + +bats_require_minimum_version 1.5.0 + +IMAGE="${IMAGE_NAME:-pygmystack/mailhog:test}" +PHP_FPM_IMAGE="${PHP_FPM_IMAGE:-uselagoon/php-8.5-fpm}" + +# Populated in setup() from files written by setup_file(). +MAILPIT_CONTAINER="" +MAILPIT_NETWORK="" +MAILPIT_ISOLATED_NETWORK="" +SMTP_PORT="" +HTTP_PORT="" + +# --------------------------------------------------------------------------- +# File-level setup / teardown +# --------------------------------------------------------------------------- + +setup_file() { + local suffix + suffix="$(openssl rand -hex 4)" + echo "${suffix}" > "${BATS_SUITE_TMPDIR}/.suffix" + + local container="mailpit-bats-test-${suffix}" + local network="mailpit-bats-net-${suffix}" + + # Pre-pull the Lagoon PHP image so it is locally cached before any test + # runs. Without this, the first test to use the image would trigger an + # inline pull, and the short default ssmtp/sendmail connection timeout + # could expire against the still-starting container. + docker pull "${PHP_FPM_IMAGE}" + + # Clean up any leftovers from a previous (failed) run. + docker rm -f "${container}" 2>/dev/null || true + docker network rm "${network}" 2>/dev/null || true + + # Create a dedicated network so the Lagoon-style PHP container tests can + # reach Mailpit via the "amazeeio-mailpit" hostname without any extra setup. + docker network create "${network}" + echo "${network}" > "${BATS_SUITE_TMPDIR}/.network" + + # Create an isolated (--internal) network for the host.docker.internal + # tests. An internal network has no host routing, so the + # `nc -z -w 1 172.17.0.1 1025` probe in 50-ssmtp.sh reliably fails even + # when a pygmy Mailpit is listening on the host machine. This ensures the + # host.docker.internal branch is the first one that can succeed. + local isolated_network + isolated_network="${network}-isolated" + docker network create --internal "${isolated_network}" + echo "${isolated_network}" > "${BATS_SUITE_TMPDIR}/.isolated_network" + + # Start Mailpit with auto-assigned host ports and on the test network. + docker run -d \ + --name "${container}" \ + --network "${network}" \ + --network-alias "amazeeio-mailpit" \ + -p 1025 \ + -p 80 \ + "${IMAGE}" + + # Also connect Mailpit to the isolated network so that host.docker.internal + # tests can reach it without going via the host. + docker network connect --alias amazeeio-mailpit "${isolated_network}" "${container}" + + # Discover the host-mapped ports (handles both 0.0.0.0:PORT and [::]:PORT). + local smtp_port http_port + smtp_port="$(docker port "${container}" 1025/tcp | grep -oE '[0-9]+$' | head -1)" + http_port="$(docker port "${container}" 80/tcp | grep -oE '[0-9]+$' | head -1)" + echo "${smtp_port}" > "${BATS_SUITE_TMPDIR}/.smtp_port" + echo "${http_port}" > "${BATS_SUITE_TMPDIR}/.http_port" + + # Wait up to 30 seconds for the HTTP API to become ready. + local max_wait=30 waited=0 + until curl -sf "http://localhost:${http_port}/api/v1/messages" >/dev/null 2>&1; do + sleep 1 + waited=$((waited + 1)) + if [ "${waited}" -ge "${max_wait}" ]; then + echo "# Timed out waiting for Mailpit HTTP API on port ${http_port}" >&3 + docker logs "${container}" >&3 2>&3 + return 1 + fi + done +} + +teardown_file() { + local suffix network isolated_network + suffix="$(cat "${BATS_SUITE_TMPDIR}/.suffix" 2>/dev/null || true)" + network="$(cat "${BATS_SUITE_TMPDIR}/.network" 2>/dev/null || true)" + isolated_network="$(cat "${BATS_SUITE_TMPDIR}/.isolated_network" 2>/dev/null || true)" + docker rm -f "mailpit-bats-test-${suffix}" 2>/dev/null || true + docker network rm "${isolated_network}" 2>/dev/null || true + docker network rm "${network}" 2>/dev/null || true +} + +# --------------------------------------------------------------------------- +# Per-test setup — restore variables from the files written by setup_file(). +# --------------------------------------------------------------------------- + +setup() { + local suffix + suffix="$(cat "${BATS_SUITE_TMPDIR}/.suffix" 2>/dev/null || true)" + MAILPIT_CONTAINER="mailpit-bats-test-${suffix}" + MAILPIT_NETWORK="$(cat "${BATS_SUITE_TMPDIR}/.network" 2>/dev/null || true)" + MAILPIT_ISOLATED_NETWORK="$(cat "${BATS_SUITE_TMPDIR}/.isolated_network" 2>/dev/null || true)" + SMTP_PORT="$(cat "${BATS_SUITE_TMPDIR}/.smtp_port" 2>/dev/null || true)" + HTTP_PORT="$(cat "${BATS_SUITE_TMPDIR}/.http_port" 2>/dev/null || true)" +} + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +# send_test_email FROM TO SUBJECT BODY +# Sends an RFC 2822 email to the running Mailpit over SMTP using curl. +send_test_email() { + local from="${1:-sender@example.com}" + local to="${2:-recipient@example.com}" + local subject="${3:-BATS Test Email}" + local body="${4:-This is a test email sent by BATS.}" + + curl --silent --show-error \ + --url "smtp://localhost:${SMTP_PORT}" \ + --mail-from "${from}" \ + --mail-rcpt "${to}" \ + --upload-file - </dev/null +} + +# message_total — returns the integer value of the "total" field from the v1 API. +message_total() { + curl -sf "http://localhost:${HTTP_PORT}/api/v1/messages" \ + | grep -o '"total":[0-9]*' | cut -d: -f2 +} + +# --------------------------------------------------------------------------- +# Container lifecycle +# --------------------------------------------------------------------------- + +@test "container is running" { + run docker inspect --format='{{.State.Status}}' "${MAILPIT_CONTAINER}" + [ "$status" -eq 0 ] + [ "$output" = "running" ] +} + +@test "mailpit process is running inside the container" { + run docker exec "${MAILPIT_CONTAINER}" sh -c 'ps | grep "[m]ailpit"' + [ "$status" -eq 0 ] + [ -n "$output" ] +} + +# --------------------------------------------------------------------------- +# HTTP UI and API +# --------------------------------------------------------------------------- + +@test "Mailpit web UI responds with HTML containing 'Mailpit'" { + run curl -sf "http://localhost:${HTTP_PORT}/" + [ "$status" -eq 0 ] + [[ "$output" =~ "Mailpit" ]] +} + +@test "Mailpit API v1 messages endpoint returns a JSON object with a 'total' field" { + run curl -sf "http://localhost:${HTTP_PORT}/api/v1/messages" + [ "$status" -eq 0 ] + [[ "$output" =~ '"total"' ]] +} + +# --------------------------------------------------------------------------- +# Email reception — empty state +# --------------------------------------------------------------------------- + +@test "message count is zero before any email is sent" { + delete_all_messages + local total + total="$(message_total)" + [ "${total}" = "0" ] +} + +# --------------------------------------------------------------------------- +# Email reception — sending via SMTP +# --------------------------------------------------------------------------- + +@test "an email sent via SMTP is captured by Mailpit" { + delete_all_messages + send_test_email \ + "sender@example.com" \ + "recipient@example.com" \ + "BATS Test Email" \ + "Hello from BATS." + local total + total="$(message_total)" + [ "${total}" = "1" ] +} + +@test "captured email has the correct From address" { + run curl -sf "http://localhost:${HTTP_PORT}/api/v1/messages" + [ "$status" -eq 0 ] + [[ "$output" =~ "sender@example.com" ]] +} + +@test "captured email has the correct To address" { + run curl -sf "http://localhost:${HTTP_PORT}/api/v1/messages" + [ "$status" -eq 0 ] + [[ "$output" =~ "recipient@example.com" ]] +} + +@test "captured email has the correct Subject" { + run curl -sf "http://localhost:${HTTP_PORT}/api/v1/messages" + [ "$status" -eq 0 ] + [[ "$output" =~ "BATS Test Email" ]] +} + +@test "captured email body contains expected content" { + run curl -sf "http://localhost:${HTTP_PORT}/api/v1/messages" + [ "$status" -eq 0 ] + [[ "$output" =~ "Hello from BATS" ]] +} + +# --------------------------------------------------------------------------- +# Email deletion +# --------------------------------------------------------------------------- + +@test "all messages can be deleted via the Mailpit API" { + run curl -sf -X DELETE "http://localhost:${HTTP_PORT}/api/v1/messages" + [ "$status" -eq 0 ] +} + +@test "message count is zero after deleting all messages" { + local total + total="$(message_total)" + [ "${total}" = "0" ] +} + +# --------------------------------------------------------------------------- +# Multiple messages +# --------------------------------------------------------------------------- + +@test "multiple emails sent via SMTP all appear in Mailpit" { + delete_all_messages + send_test_email "a@example.com" "x@example.com" "Email One" "Body one." + send_test_email "b@example.com" "y@example.com" "Email Two" "Body two." + send_test_email "c@example.com" "z@example.com" "Email Three" "Body three." + local total + total="$(message_total)" + [ "${total}" = "3" ] + delete_all_messages +} + +# --------------------------------------------------------------------------- +# Lagoon-style email send — simulates the 50-ssmtp.sh entrypoint +# +# Uses the real uselagoon/php-8.5-fpm image with ssmtp installed and the +# 50-ssmtp.sh entrypoint at /lagoon/entrypoints/50-ssmtp.sh. The script is +# dot-sourced so its `return` statements are handled correctly, then the +# ssmtp sendmail binary delivers the message — exactly as a live Lagoon PHP +# container would when SSMTP_MAILHUB is set by an operator. +# --------------------------------------------------------------------------- + +@test "email sent via Lagoon PHP container (ssmtp) is captured by Mailpit" { + delete_all_messages + + # Pass SSMTP_MAILHUB so 50-ssmtp.sh writes mailhub=amazeeio-mailpit:1025 + # into /etc/ssmtp/ssmtp.conf, then send via the real ssmtp sendmail binary. + run docker run --rm \ + --network "${MAILPIT_NETWORK}" \ + -e "SSMTP_MAILHUB=amazeeio-mailpit:1025" \ + --entrypoint sh \ + "${PHP_FPM_IMAGE}" -euc ' + . /lagoon/entrypoints/50-ssmtp.sh + printf "To: dev@example.com\nFrom: lagoon-test@example.com\nSubject: Lagoon BATS Test\n\nSent via ssmtp as Lagoon would.\n" \ + | sendmail -t + ' + [ "$status" -eq 0 ] + + # Verify the message arrived in Mailpit. + local total + total="$(message_total)" + [ "${total}" -ge "1" ] + + run curl -sf "http://localhost:${HTTP_PORT}/api/v1/messages" + [ "$status" -eq 0 ] + [[ "$output" =~ "Lagoon BATS Test" ]] + + delete_all_messages +} + +# --------------------------------------------------------------------------- +# SSMTP_MAILHUB env var — simulates lines 20-21 of 50-ssmtp.sh +# +# When SSMTP_MAILHUB is explicitly set, Lagoon's 50-ssmtp.sh writes: +# mailhub=${SSMTP_MAILHUB} +# directly into /etc/ssmtp/ssmtp.conf and skips all auto-detection. +# These tests verify that a client honouring SSMTP_MAILHUB successfully +# delivers mail to Mailpit using the configured hub value. +# --------------------------------------------------------------------------- + +@test "email sent with SSMTP_MAILHUB set to 'amazeeio-mailpit:1025' is captured by Mailpit" { + delete_all_messages + + # The real 50-ssmtp.sh writes "mailhub=${SSMTP_MAILHUB}" into + # /etc/ssmtp/ssmtp.conf when SSMTP_MAILHUB is set (lines 20-21). Dot- + # source so `return` is handled correctly, then send via ssmtp sendmail. + run docker run --rm \ + --network "${MAILPIT_NETWORK}" \ + -e "SSMTP_MAILHUB=amazeeio-mailpit:1025" \ + --entrypoint sh \ + "${PHP_FPM_IMAGE}" -euc ' + . /lagoon/entrypoints/50-ssmtp.sh + printf "To: dev@example.com\nFrom: ssmtp-mailhub-test@example.com\nSubject: SSMTP_MAILHUB Test\n\nSent via SSMTP_MAILHUB.\n" \ + | sendmail -t + ' + [ "$status" -eq 0 ] + + local total + total="$(message_total)" + [ "${total}" -ge "1" ] + + run curl -sf "http://localhost:${HTTP_PORT}/api/v1/messages" + [ "$status" -eq 0 ] + [[ "$output" =~ "SSMTP_MAILHUB Test" ]] + + delete_all_messages +} + +@test "email sent with SSMTP_MAILHUB overrides auto-detection and reaches Mailpit" { + delete_all_messages + + # Confirms the override semantics: SSMTP_MAILHUB is the first branch in + # 50-ssmtp.sh's if/elif chain so it short-circuits all auto-detection + # (172.17.0.1, host.docker.internal, LAGOON_PROJECT) regardless of what + # else might be reachable on the network. + run docker run --rm \ + --network "${MAILPIT_NETWORK}" \ + -e "SSMTP_MAILHUB=amazeeio-mailpit:1025" \ + --entrypoint sh \ + "${PHP_FPM_IMAGE}" -euc ' + . /lagoon/entrypoints/50-ssmtp.sh + printf "To: dev@example.com\nFrom: override-test@example.com\nSubject: SSMTP_MAILHUB Override Test\n\nSMTP_MAILHUB takes priority.\n" \ + | sendmail -t + ' + [ "$status" -eq 0 ] + + run curl -sf "http://localhost:${HTTP_PORT}/api/v1/messages" + [ "$status" -eq 0 ] + [[ "$output" =~ "SSMTP_MAILHUB Override Test" ]] + + delete_all_messages +} + +# --------------------------------------------------------------------------- +# host.docker.internal — simulates lines 26-29 of 50-ssmtp.sh +# +# When neither SSMTP_MAILHUB nor 172.17.0.1:1025 is available, Lagoon's +# 50-ssmtp.sh tries `nc -z -w 1 host.docker.internal 1025`. If that +# succeeds it writes "mailhub=host.docker.internal:1025" into ssmtp.conf. +# +# An --internal Docker network is used here so that the 172.17.0.1:1025 nc +# probe reliably fails even when a pygmy Mailpit is listening on the host +# machine (internal networks have no host routing). The Mailpit container is +# connected to both the regular and the isolated network; its IP on the isolated +# network is injected as host.docker.internal via --add-host so that the nc +# probe and ssmtp sendmail both resolve to the BATS Mailpit. +# --------------------------------------------------------------------------- + +@test "email sent via host.docker.internal route is captured by Mailpit" { + delete_all_messages + + # Use the isolated (--internal) network so that the nc probe to + # 172.17.0.1:1025 in 50-ssmtp.sh reliably fails — even when a host-level + # pygmy Mailpit is running — and the host.docker.internal branch becomes + # the first probe to succeed. The Mailpit IP on the isolated network is + # injected via --add-host so that nc and ssmtp both resolve it correctly. + local mailpit_isolated_ip + mailpit_isolated_ip="$(docker inspect \ + --format="{{(index .NetworkSettings.Networks \"${MAILPIT_ISOLATED_NETWORK}\").IPAddress}}" \ + "${MAILPIT_CONTAINER}")" + + run docker run --rm \ + --network "${MAILPIT_ISOLATED_NETWORK}" \ + --add-host "host.docker.internal:${mailpit_isolated_ip}" \ + --entrypoint sh \ + "${PHP_FPM_IMAGE}" -euc ' + . /lagoon/entrypoints/50-ssmtp.sh + printf "To: dev@example.com\nFrom: hdi-test@example.com\nSubject: host.docker.internal Test\n\nSent via host.docker.internal.\n" \ + | sendmail -t + ' + [ "$status" -eq 0 ] + + local total + total="$(message_total)" + [ "${total}" -ge "1" ] + + run curl -sf "http://localhost:${HTTP_PORT}/api/v1/messages" + [ "$status" -eq 0 ] + [[ "$output" =~ "host.docker.internal Test" ]] + + delete_all_messages +} + +@test "nc probe to host.docker.internal:1025 succeeds when Mailpit is reachable" { + # Verify the nc connectivity check itself — the condition that 50-ssmtp.sh + # evaluates before writing mailhub=host.docker.internal:1025. + local mailpit_isolated_ip + mailpit_isolated_ip="$(docker inspect \ + --format="{{(index .NetworkSettings.Networks \"${MAILPIT_ISOLATED_NETWORK}\").IPAddress}}" \ + "${MAILPIT_CONTAINER}")" + + run docker run --rm \ + --network "${MAILPIT_ISOLATED_NETWORK}" \ + --add-host "host.docker.internal:${mailpit_isolated_ip}" \ + --entrypoint sh \ + "${PHP_FPM_IMAGE}" -euc 'nc -z -w 1 host.docker.internal 1025' + [ "$status" -eq 0 ] +}