From 65f33f8ae9e419e99ad29aa75b803c0b319ac540 Mon Sep 17 00:00:00 2001 From: Toby Bellwood Date: Wed, 18 Mar 2026 12:22:53 +1100 Subject: [PATCH 1/4] chore: update Dockerfile and BATS tests for MailHog --- .github/workflows/build_and_test.yml | 58 ++-- Dockerfile | 4 +- Makefile | 52 ++++ tests/image_structure.bats | 58 ++++ tests/runtime.bats | 448 +++++++++++++++++++++++++++ 5 files changed, 600 insertions(+), 20 deletions(-) create mode 100644 Makefile create mode 100644 tests/image_structure.bats create mode 100644 tests/runtime.bats diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 9765be8..9d6ffba 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 @@ -64,7 +64,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # Establish some SSH keys. - name: Setup SSH @@ -77,29 +77,36 @@ 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: Install pygmy, dockerize and bats-core via brew + 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 bats-core; brew install pygmy; brew install dockerize; echo "/home/linuxbrew/.linuxbrew/bin" >> $GITHUB_PATH; @@ -117,6 +124,21 @@ jobs: name: Show pygmy image versions run: | docker ps -a --filter "label=pygmy.name" + - + name: Pull image for BATS tests + run: | + docker pull ${{ steps.single_tag.outputs.tag }} + docker pull uselagoon/php-8.5-fpm + - + name: Run BATS image structure tests + env: + IMAGE_NAME: ${{ steps.single_tag.outputs.tag }} + run: bats --tap tests/image_structure.bats + - + name: Run BATS runtime tests + env: + IMAGE_NAME: ${{ steps.single_tag.outputs.tag }} + run: bats --tap tests/runtime.bats - name: Export and show configuration - pygmy.basic.yml run: | diff --git a/Dockerfile b/Dockerfile index 5c253be..91faa48 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ -FROM golang:1.21-alpine3.19 as builder +FROM golang:1.25-alpine3.23 AS builder RUN apk --no-cache add --virtual \ build-dependencies \ git \ && GOPATH=/tmp/gocode go install github.com/mailhog/MailHog@v1.0.1 -FROM alpine:3.19 +FROM alpine:3.23 WORKDIR /bin COPY --from=builder tmp/gocode/bin/MailHog /bin/MailHog EXPOSE 1025 8025 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..f465f0f --- /dev/null +++ b/tests/image_structure.bats @@ -0,0 +1,58 @@ +#!/usr/bin/env bats +# Image structure tests — verify the binary, exposed ports, entrypoint, and +# working directory baked into the MailHog 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 "MailHog binary is present at /bin/MailHog" { + run docker run --rm --entrypoint sh "${IMAGE}" -c 'test -f /bin/MailHog' + [ "$status" -eq 0 ] +} + +@test "MailHog binary is executable" { + run docker run --rm --entrypoint sh "${IMAGE}" -c 'test -x /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 8025 as exposed" { + run docker inspect --format='{{json .Config.ExposedPorts}}' "${IMAGE}" + [ "$status" -eq 0 ] + [[ "$output" =~ "8025" ]] +} + +# --------------------------------------------------------------------------- +# Entrypoint +# --------------------------------------------------------------------------- + +@test "image entrypoint is MailHog" { + run docker inspect --format='{{json .Config.Entrypoint}}' "${IMAGE}" + [ "$status" -eq 0 ] + [[ "$output" =~ "MailHog" ]] +} + +# --------------------------------------------------------------------------- +# Working directory +# --------------------------------------------------------------------------- + +@test "image working directory is /bin" { + run docker inspect --format='{{.Config.WorkingDir}}' "${IMAGE}" + [ "$status" -eq 0 ] + [ "$output" = "/bin" ] +} diff --git a/tests/runtime.bats b/tests/runtime.bats new file mode 100644 index 0000000..ff2dc2e --- /dev/null +++ b/tests/runtime.bats @@ -0,0 +1,448 @@ +#!/usr/bin/env bats +# Runtime tests — start a MailHog 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 MailHog via the well-known +# hostname "amazeeio-mailhog" (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(). +MAILHOG_CONTAINER="" +MAILHOG_NETWORK="" +MAILHOG_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="mailhog-bats-test-${suffix}" + local network="mailhog-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 MailHog via the "amazeeio-mailhog" 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 MailHog 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 MailHog with auto-assigned host ports and on the test network. + docker run -d \ + --name "${container}" \ + --network "${network}" \ + --network-alias "amazeeio-mailhog" \ + -p 1025 \ + -p 8025 \ + "${IMAGE}" + + # Also connect MailHog to the isolated network so that host.docker.internal + # tests can reach it without going via the host. + docker network connect --alias amazeeio-mailhog "${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}" 8025/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/v2/messages" >/dev/null 2>&1; do + sleep 1 + waited=$((waited + 1)) + if [ "${waited}" -ge "${max_wait}" ]; then + echo "# Timed out waiting for MailHog 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 "mailhog-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)" + MAILHOG_CONTAINER="mailhog-bats-test-${suffix}" + MAILHOG_NETWORK="$(cat "${BATS_SUITE_TMPDIR}/.network" 2>/dev/null || true)" + MAILHOG_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 MailHog 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 v2 API. +message_total() { + curl -sf "http://localhost:${HTTP_PORT}/api/v2/messages" \ + | grep -o '"total":[0-9]*' | cut -d: -f2 +} + +# --------------------------------------------------------------------------- +# Container lifecycle +# --------------------------------------------------------------------------- + +@test "container is running" { + run docker inspect --format='{{.State.Status}}' "${MAILHOG_CONTAINER}" + [ "$status" -eq 0 ] + [ "$output" = "running" ] +} + +@test "MailHog process is running inside the container" { + run docker exec "${MAILHOG_CONTAINER}" sh -c 'ps | grep "[M]ailHog"' + [ "$status" -eq 0 ] + [ -n "$output" ] +} + +# --------------------------------------------------------------------------- +# HTTP UI and API +# --------------------------------------------------------------------------- + +@test "MailHog web UI responds with HTML containing 'MailHog'" { + run curl -sf "http://localhost:${HTTP_PORT}/" + [ "$status" -eq 0 ] + [[ "$output" =~ "MailHog" ]] +} + +@test "MailHog API v2 messages endpoint returns a JSON response with a 'total' field" { + run curl -sf "http://localhost:${HTTP_PORT}/api/v2/messages" + [ "$status" -eq 0 ] + [[ "$output" =~ "total" ]] +} + +@test "MailHog API v1 messages endpoint returns a JSON array or null" { + run curl -sf "http://localhost:${HTTP_PORT}/api/v1/messages" + [ "$status" -eq 0 ] + # Response is a JSON array when messages exist, or null when empty. + [[ "$output" =~ ^\[ ]] || [ "$output" = "null" ] +} + +# --------------------------------------------------------------------------- +# 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 MailHog" { + 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 MailHog 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 MailHog" { + 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 MailHog" { + delete_all_messages + + # Pass SSMTP_MAILHUB so 50-ssmtp.sh writes mailhub=amazeeio-mailhog:1025 + # into /etc/ssmtp/ssmtp.conf, then send via the real ssmtp sendmail binary. + run docker run --rm \ + --network "${MAILHOG_NETWORK}" \ + -e "SSMTP_MAILHUB=amazeeio-mailhog: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 MailHog. + 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 MailHog using the configured hub value. +# --------------------------------------------------------------------------- + +@test "email sent with SSMTP_MAILHUB set to 'amazeeio-mailhog:1025' is captured by MailHog" { + 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 "${MAILHOG_NETWORK}" \ + -e "SSMTP_MAILHUB=amazeeio-mailhog: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 MailHog" { + 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 "${MAILHOG_NETWORK}" \ + -e "SSMTP_MAILHUB=amazeeio-mailhog: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 MailHog is listening on the host +# machine (internal networks have no host routing). The MailHog 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 MailHog. +# --------------------------------------------------------------------------- + +@test "email sent via host.docker.internal route is captured by MailHog" { + 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 MailHog is running — and the host.docker.internal branch becomes + # the first probe to succeed. The MailHog IP on the isolated network is + # injected via --add-host so that nc and ssmtp both resolve it correctly. + local mailhog_isolated_ip + mailhog_isolated_ip="$(docker inspect \ + --format="{{(index .NetworkSettings.Networks \"${MAILHOG_ISOLATED_NETWORK}\").IPAddress}}" \ + "${MAILHOG_CONTAINER}")" + + run docker run --rm \ + --network "${MAILHOG_ISOLATED_NETWORK}" \ + --add-host "host.docker.internal:${mailhog_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 MailHog is reachable" { + # Verify the nc connectivity check itself — the condition that 50-ssmtp.sh + # evaluates before writing mailhub=host.docker.internal:1025. + local mailhog_ip + mailhog_ip="$(docker inspect \ + --format="{{(index .NetworkSettings.Networks \"${MAILHOG_NETWORK}\").IPAddress}}" \ + "${MAILHOG_CONTAINER}")" + + local mailhog_isolated_ip + mailhog_isolated_ip="$(docker inspect \ + --format="{{(index .NetworkSettings.Networks \"${MAILHOG_ISOLATED_NETWORK}\").IPAddress}}" \ + "${MAILHOG_CONTAINER}")" + + run docker run --rm \ + --network "${MAILHOG_ISOLATED_NETWORK}" \ + --add-host "host.docker.internal:${mailhog_isolated_ip}" \ + --entrypoint sh \ + "${PHP_FPM_IMAGE}" -euc 'nc -z -w 1 host.docker.internal 1025' + [ "$status" -eq 0 ] +} From 8ad34e949f31f619de82184823daafc1850e3d68 Mon Sep 17 00:00:00 2001 From: Toby Bellwood Date: Wed, 18 Mar 2026 12:35:59 +1100 Subject: [PATCH 2/4] test: fixup actions docker-compose --- .github/workflows/build_and_test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 9d6ffba..7d5fc0a 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -223,14 +223,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 From 2114f796c37097809b97e3df51ee50ac3da1b1e0 Mon Sep 17 00:00:00 2001 From: Toby Bellwood Date: Wed, 18 Mar 2026 12:41:11 +1100 Subject: [PATCH 3/4] test: reorder bats tests --- .github/workflows/build_and_test.yml | 44 +++++++++++++--------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 7d5fc0a..b275627 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -61,7 +61,7 @@ jobs: test: needs: docker - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -98,19 +98,32 @@ jobs: find examples/ -name "*.bak" -delete grep -Fn ghcr examples/* - - name: Install pygmy, dockerize and bats-core 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 bats-core; - brew install pygmy; 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: | @@ -124,28 +137,13 @@ jobs: name: Show pygmy image versions run: | docker ps -a --filter "label=pygmy.name" - - - name: Pull image for BATS tests - run: | - docker pull ${{ steps.single_tag.outputs.tag }} - docker pull uselagoon/php-8.5-fpm - - - name: Run BATS image structure tests - env: - IMAGE_NAME: ${{ steps.single_tag.outputs.tag }} - run: bats --tap tests/image_structure.bats - - - name: Run BATS runtime tests - env: - IMAGE_NAME: ${{ steps.single_tag.outputs.tag }} - run: bats --tap tests/runtime.bats - name: Export and show configuration - pygmy.basic.yml run: | 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: | @@ -207,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: | From a8cc8261c65810d890faf2d540eb51b3092a5c3e Mon Sep 17 00:00:00 2001 From: Toby Bellwood Date: Wed, 18 Mar 2026 15:23:07 +1100 Subject: [PATCH 4/4] chore: move to mailpit --- .github/workflows/build_and_test.yml | 4 +- .vscode/settings.json | 5 + Dockerfile | 15 +-- tests/image_structure.bats | 27 +++-- tests/runtime.bats | 146 ++++++++++++--------------- 5 files changed, 95 insertions(+), 102 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index b275627..ef275ba 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -156,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: | 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 91faa48..ce683ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,7 @@ -FROM golang:1.25-alpine3.23 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.23 -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/tests/image_structure.bats b/tests/image_structure.bats index f465f0f..d3c1469 100644 --- a/tests/image_structure.bats +++ b/tests/image_structure.bats @@ -1,6 +1,6 @@ #!/usr/bin/env bats # Image structure tests — verify the binary, exposed ports, entrypoint, and -# working directory baked into the MailHog image. Tests run ephemeral +# 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 @@ -11,13 +11,18 @@ IMAGE="${IMAGE_NAME:-pygmystack/mailhog:test}" # Binary # --------------------------------------------------------------------------- -@test "MailHog binary is present at /bin/MailHog" { - run docker run --rm --entrypoint sh "${IMAGE}" -c 'test -f /bin/MailHog' +@test "mailpit binary is present at /mailpit" { + run docker run --rm --entrypoint sh "${IMAGE}" -c 'test -f /mailpit' [ "$status" -eq 0 ] } -@test "MailHog binary is executable" { - run docker run --rm --entrypoint sh "${IMAGE}" -c 'test -x /bin/MailHog' +@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 ] } @@ -31,28 +36,28 @@ IMAGE="${IMAGE_NAME:-pygmystack/mailhog:test}" [[ "$output" =~ "1025" ]] } -@test "image declares HTTP port 8025 as exposed" { +@test "image declares HTTP port 80 as exposed" { run docker inspect --format='{{json .Config.ExposedPorts}}' "${IMAGE}" [ "$status" -eq 0 ] - [[ "$output" =~ "8025" ]] + [[ "$output" =~ "\"80" ]] } # --------------------------------------------------------------------------- # Entrypoint # --------------------------------------------------------------------------- -@test "image entrypoint is MailHog" { +@test "image entrypoint is mailpit" { run docker inspect --format='{{json .Config.Entrypoint}}' "${IMAGE}" [ "$status" -eq 0 ] - [[ "$output" =~ "MailHog" ]] + [[ "$output" =~ "mailpit" ]] } # --------------------------------------------------------------------------- # Working directory # --------------------------------------------------------------------------- -@test "image working directory is /bin" { +@test "image working directory is /" { run docker inspect --format='{{.Config.WorkingDir}}' "${IMAGE}" [ "$status" -eq 0 ] - [ "$output" = "/bin" ] + [ "$output" = "/" ] } diff --git a/tests/runtime.bats b/tests/runtime.bats index ff2dc2e..57eebd9 100644 --- a/tests/runtime.bats +++ b/tests/runtime.bats @@ -1,11 +1,11 @@ #!/usr/bin/env bats -# Runtime tests — start a MailHog container and exercise SMTP reception and +# 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 MailHog via the well-known -# hostname "amazeeio-mailhog" (matching the alias used by Lagoon's 50-ssmtp.sh +# 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). @@ -19,9 +19,9 @@ 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(). -MAILHOG_CONTAINER="" -MAILHOG_NETWORK="" -MAILHOG_ISOLATED_NETWORK="" +MAILPIT_CONTAINER="" +MAILPIT_NETWORK="" +MAILPIT_ISOLATED_NETWORK="" SMTP_PORT="" HTTP_PORT="" @@ -34,8 +34,8 @@ setup_file() { suffix="$(openssl rand -hex 4)" echo "${suffix}" > "${BATS_SUITE_TMPDIR}/.suffix" - local container="mailhog-bats-test-${suffix}" - local network="mailhog-bats-net-${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 @@ -48,47 +48,47 @@ setup_file() { docker network rm "${network}" 2>/dev/null || true # Create a dedicated network so the Lagoon-style PHP container tests can - # reach MailHog via the "amazeeio-mailhog" hostname without any extra setup. + # 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 MailHog is listening on the host machine. This ensures the + # 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 MailHog with auto-assigned host ports and on the test network. + # Start Mailpit with auto-assigned host ports and on the test network. docker run -d \ --name "${container}" \ --network "${network}" \ - --network-alias "amazeeio-mailhog" \ + --network-alias "amazeeio-mailpit" \ -p 1025 \ - -p 8025 \ + -p 80 \ "${IMAGE}" - # Also connect MailHog to the isolated network so that host.docker.internal + # 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-mailhog "${isolated_network}" "${container}" + 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}" 8025/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/v2/messages" >/dev/null 2>&1; do + 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 MailHog HTTP API on port ${http_port}" >&3 + echo "# Timed out waiting for Mailpit HTTP API on port ${http_port}" >&3 docker logs "${container}" >&3 2>&3 return 1 fi @@ -100,7 +100,7 @@ teardown_file() { 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 "mailhog-bats-test-${suffix}" 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 } @@ -112,9 +112,9 @@ teardown_file() { setup() { local suffix suffix="$(cat "${BATS_SUITE_TMPDIR}/.suffix" 2>/dev/null || true)" - MAILHOG_CONTAINER="mailhog-bats-test-${suffix}" - MAILHOG_NETWORK="$(cat "${BATS_SUITE_TMPDIR}/.network" 2>/dev/null || true)" - MAILHOG_ISOLATED_NETWORK="$(cat "${BATS_SUITE_TMPDIR}/.isolated_network" 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)" } @@ -124,7 +124,7 @@ setup() { # --------------------------------------------------------------------------- # send_test_email FROM TO SUBJECT BODY -# Sends an RFC 2822 email to the running MailHog over SMTP using curl. +# 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}" @@ -144,14 +144,14 @@ ${body} EOF } -# delete_all_messages — purges every message from MailHog via its API. +# delete_all_messages — purges every message from Mailpit via its API. delete_all_messages() { curl -sf -X DELETE "http://localhost:${HTTP_PORT}/api/v1/messages" >/dev/null } -# message_total — returns the integer value of the "total" field from the v2 API. +# message_total — returns the integer value of the "total" field from the v1 API. message_total() { - curl -sf "http://localhost:${HTTP_PORT}/api/v2/messages" \ + curl -sf "http://localhost:${HTTP_PORT}/api/v1/messages" \ | grep -o '"total":[0-9]*' | cut -d: -f2 } @@ -160,13 +160,13 @@ message_total() { # --------------------------------------------------------------------------- @test "container is running" { - run docker inspect --format='{{.State.Status}}' "${MAILHOG_CONTAINER}" + run docker inspect --format='{{.State.Status}}' "${MAILPIT_CONTAINER}" [ "$status" -eq 0 ] [ "$output" = "running" ] } -@test "MailHog process is running inside the container" { - run docker exec "${MAILHOG_CONTAINER}" sh -c 'ps | grep "[M]ailHog"' +@test "mailpit process is running inside the container" { + run docker exec "${MAILPIT_CONTAINER}" sh -c 'ps | grep "[m]ailpit"' [ "$status" -eq 0 ] [ -n "$output" ] } @@ -175,23 +175,16 @@ message_total() { # HTTP UI and API # --------------------------------------------------------------------------- -@test "MailHog web UI responds with HTML containing 'MailHog'" { +@test "Mailpit web UI responds with HTML containing 'Mailpit'" { run curl -sf "http://localhost:${HTTP_PORT}/" [ "$status" -eq 0 ] - [[ "$output" =~ "MailHog" ]] + [[ "$output" =~ "Mailpit" ]] } -@test "MailHog API v2 messages endpoint returns a JSON response with a 'total' field" { - run curl -sf "http://localhost:${HTTP_PORT}/api/v2/messages" - [ "$status" -eq 0 ] - [[ "$output" =~ "total" ]] -} - -@test "MailHog API v1 messages endpoint returns a JSON array or null" { +@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 ] - # Response is a JSON array when messages exist, or null when empty. - [[ "$output" =~ ^\[ ]] || [ "$output" = "null" ] + [[ "$output" =~ '"total"' ]] } # --------------------------------------------------------------------------- @@ -209,7 +202,7 @@ message_total() { # Email reception — sending via SMTP # --------------------------------------------------------------------------- -@test "an email sent via SMTP is captured by MailHog" { +@test "an email sent via SMTP is captured by Mailpit" { delete_all_messages send_test_email \ "sender@example.com" \ @@ -249,7 +242,7 @@ message_total() { # Email deletion # --------------------------------------------------------------------------- -@test "all messages can be deleted via the MailHog API" { +@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 ] } @@ -264,7 +257,7 @@ message_total() { # Multiple messages # --------------------------------------------------------------------------- -@test "multiple emails sent via SMTP all appear in MailHog" { +@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." @@ -285,14 +278,14 @@ message_total() { # container would when SSMTP_MAILHUB is set by an operator. # --------------------------------------------------------------------------- -@test "email sent via Lagoon PHP container (ssmtp) is captured by MailHog" { +@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-mailhog:1025 + # 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 "${MAILHOG_NETWORK}" \ - -e "SSMTP_MAILHUB=amazeeio-mailhog:1025" \ + --network "${MAILPIT_NETWORK}" \ + -e "SSMTP_MAILHUB=amazeeio-mailpit:1025" \ --entrypoint sh \ "${PHP_FPM_IMAGE}" -euc ' . /lagoon/entrypoints/50-ssmtp.sh @@ -301,7 +294,7 @@ message_total() { ' [ "$status" -eq 0 ] - # Verify the message arrived in MailHog. + # Verify the message arrived in Mailpit. local total total="$(message_total)" [ "${total}" -ge "1" ] @@ -320,18 +313,18 @@ message_total() { # 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 MailHog using the configured hub value. +# delivers mail to Mailpit using the configured hub value. # --------------------------------------------------------------------------- -@test "email sent with SSMTP_MAILHUB set to 'amazeeio-mailhog:1025' is captured by MailHog" { +@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 "${MAILHOG_NETWORK}" \ - -e "SSMTP_MAILHUB=amazeeio-mailhog:1025" \ + --network "${MAILPIT_NETWORK}" \ + -e "SSMTP_MAILHUB=amazeeio-mailpit:1025" \ --entrypoint sh \ "${PHP_FPM_IMAGE}" -euc ' . /lagoon/entrypoints/50-ssmtp.sh @@ -351,7 +344,7 @@ message_total() { delete_all_messages } -@test "email sent with SSMTP_MAILHUB overrides auto-detection and reaches MailHog" { +@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 @@ -359,8 +352,8 @@ message_total() { # (172.17.0.1, host.docker.internal, LAGOON_PROJECT) regardless of what # else might be reachable on the network. run docker run --rm \ - --network "${MAILHOG_NETWORK}" \ - -e "SSMTP_MAILHUB=amazeeio-mailhog:1025" \ + --network "${MAILPIT_NETWORK}" \ + -e "SSMTP_MAILHUB=amazeeio-mailpit:1025" \ --entrypoint sh \ "${PHP_FPM_IMAGE}" -euc ' . /lagoon/entrypoints/50-ssmtp.sh @@ -384,29 +377,29 @@ message_total() { # 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 MailHog is listening on the host -# machine (internal networks have no host routing). The MailHog container is +# 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 MailHog. +# probe and ssmtp sendmail both resolve to the BATS Mailpit. # --------------------------------------------------------------------------- -@test "email sent via host.docker.internal route is captured by MailHog" { +@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 MailHog is running — and the host.docker.internal branch becomes - # the first probe to succeed. The MailHog IP on the isolated network is + # 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 mailhog_isolated_ip - mailhog_isolated_ip="$(docker inspect \ - --format="{{(index .NetworkSettings.Networks \"${MAILHOG_ISOLATED_NETWORK}\").IPAddress}}" \ - "${MAILHOG_CONTAINER}")" + local mailpit_isolated_ip + mailpit_isolated_ip="$(docker inspect \ + --format="{{(index .NetworkSettings.Networks \"${MAILPIT_ISOLATED_NETWORK}\").IPAddress}}" \ + "${MAILPIT_CONTAINER}")" run docker run --rm \ - --network "${MAILHOG_ISOLATED_NETWORK}" \ - --add-host "host.docker.internal:${mailhog_isolated_ip}" \ + --network "${MAILPIT_ISOLATED_NETWORK}" \ + --add-host "host.docker.internal:${mailpit_isolated_ip}" \ --entrypoint sh \ "${PHP_FPM_IMAGE}" -euc ' . /lagoon/entrypoints/50-ssmtp.sh @@ -426,22 +419,17 @@ message_total() { delete_all_messages } -@test "nc probe to host.docker.internal:1025 succeeds when MailHog is reachable" { +@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 mailhog_ip - mailhog_ip="$(docker inspect \ - --format="{{(index .NetworkSettings.Networks \"${MAILHOG_NETWORK}\").IPAddress}}" \ - "${MAILHOG_CONTAINER}")" - - local mailhog_isolated_ip - mailhog_isolated_ip="$(docker inspect \ - --format="{{(index .NetworkSettings.Networks \"${MAILHOG_ISOLATED_NETWORK}\").IPAddress}}" \ - "${MAILHOG_CONTAINER}")" + local mailpit_isolated_ip + mailpit_isolated_ip="$(docker inspect \ + --format="{{(index .NetworkSettings.Networks \"${MAILPIT_ISOLATED_NETWORK}\").IPAddress}}" \ + "${MAILPIT_CONTAINER}")" run docker run --rm \ - --network "${MAILHOG_ISOLATED_NETWORK}" \ - --add-host "host.docker.internal:${mailhog_isolated_ip}" \ + --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 ]