✨ feat(bdd): pkg/bdd/mailpit/ HTTP client + integration tests (ADR-0030 Phase A.2) (#60)
Co-authored-by: Gabriel Radureau <arcodange@gmail.com> Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
This commit was merged in pull request #60.
This commit is contained in:
133
pkg/bdd/mailpit/client_integration_test.go
Normal file
133
pkg/bdd/mailpit/client_integration_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
//go:build integration
|
||||
|
||||
// Integration tests for the Mailpit client. Run with:
|
||||
//
|
||||
// go test -tags integration ./pkg/bdd/mailpit/...
|
||||
//
|
||||
// Requires a running Mailpit reachable at http://localhost:8025
|
||||
// (the docker-compose service from ADR-0029).
|
||||
package mailpit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// uniqueRecipient returns an address unique to this test run, using the
|
||||
// per-scenario-recipient pattern from ADR-0030. Two parallel test runs
|
||||
// generate different suffixes so they never see each other's messages.
|
||||
func uniqueRecipient(t *testing.T) string {
|
||||
t.Helper()
|
||||
var raw [4]byte
|
||||
_, err := rand.Read(raw[:])
|
||||
require.NoError(t, err)
|
||||
return "integ-" + t.Name() + "-" + hex.EncodeToString(raw[:]) + "@bdd.local"
|
||||
}
|
||||
|
||||
// sendViaSMTP submits a small email through Mailpit's SMTP port.
|
||||
// Real-wire-format path : same as the application code will use.
|
||||
func sendViaSMTP(t *testing.T, to, subject, body string) {
|
||||
t.Helper()
|
||||
from := "integ-test@bdd.local"
|
||||
msg := []byte(
|
||||
"From: " + from + "\r\n" +
|
||||
"To: " + to + "\r\n" +
|
||||
"Subject: " + subject + "\r\n" +
|
||||
"\r\n" +
|
||||
body + "\r\n",
|
||||
)
|
||||
err := smtp.SendMail("localhost:1025", nil, from, []string{to}, msg)
|
||||
require.NoError(t, err, "SMTP send to local Mailpit")
|
||||
}
|
||||
|
||||
// TestIntegration_RoundTrip validates the full path : SMTP submit →
|
||||
// Mailpit captures → client lists → client gets full body. This is
|
||||
// the smoke test for the BDD-helper contract.
|
||||
func TestIntegration_RoundTrip(t *testing.T) {
|
||||
c := NewClient()
|
||||
to := uniqueRecipient(t)
|
||||
|
||||
// Defensive cleanup before the test (in case the recipient was reused)
|
||||
require.NoError(t, c.PurgeMessagesTo(context.Background(), to))
|
||||
|
||||
subject := "Integration roundtrip"
|
||||
body := "Token: integ-token-" + strings.ReplaceAll(to, "@", "-at-")
|
||||
|
||||
sendViaSMTP(t, to, subject, body)
|
||||
|
||||
msg, err := c.AwaitMessageTo(context.Background(), to, 3*time.Second)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, msg)
|
||||
|
||||
assert.Equal(t, subject, msg.Subject)
|
||||
assert.Contains(t, msg.Text, "Token: integ-token-")
|
||||
if assert.Len(t, msg.To, 1) {
|
||||
assert.Equal(t, to, msg.To[0].Address)
|
||||
}
|
||||
|
||||
// Cleanup so subsequent runs of this same test name don't accumulate
|
||||
require.NoError(t, c.PurgeMessagesTo(context.Background(), to))
|
||||
}
|
||||
|
||||
// TestIntegration_AwaitTimeoutWhenNoMessage confirms AwaitMessageTo
|
||||
// returns an error within the timeout when no message arrives.
|
||||
func TestIntegration_AwaitTimeoutWhenNoMessage(t *testing.T) {
|
||||
c := NewClient()
|
||||
to := uniqueRecipient(t) // never sent to → must time out
|
||||
|
||||
start := time.Now()
|
||||
_, err := c.AwaitMessageTo(context.Background(), to, 200*time.Millisecond)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no message")
|
||||
assert.GreaterOrEqual(t, elapsed, 150*time.Millisecond, "should poll until close to timeout")
|
||||
assert.Less(t, elapsed, 1*time.Second, "should not exceed timeout substantially")
|
||||
}
|
||||
|
||||
// TestIntegration_PurgeIsolation proves the per-recipient query/delete
|
||||
// model from ADR-0030 : two unique recipients can have their own
|
||||
// messages without one's purge affecting the other.
|
||||
func TestIntegration_PurgeIsolation(t *testing.T) {
|
||||
c := NewClient()
|
||||
// Build two distinct, well-formed addresses (separate local-parts,
|
||||
// same domain). Avoid mutating uniqueRecipient's output post-@.
|
||||
var rawA, rawB [4]byte
|
||||
_, _ = rand.Read(rawA[:])
|
||||
_, _ = rand.Read(rawB[:])
|
||||
toA := "iso-a-" + hex.EncodeToString(rawA[:]) + "@bdd.local"
|
||||
toB := "iso-b-" + hex.EncodeToString(rawB[:]) + "@bdd.local"
|
||||
|
||||
sendViaSMTP(t, toA, "for A", "body A")
|
||||
sendViaSMTP(t, toB, "for B", "body B")
|
||||
|
||||
// Both messages should exist
|
||||
idsA, err := c.MessagesTo(context.Background(), toA)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(idsA), 1, "A should have its message")
|
||||
idsB, err := c.MessagesTo(context.Background(), toB)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(idsB), 1, "B should have its message")
|
||||
|
||||
// Purge A only
|
||||
require.NoError(t, c.PurgeMessagesTo(context.Background(), toA))
|
||||
|
||||
// A is empty, B is untouched
|
||||
idsA, err = c.MessagesTo(context.Background(), toA)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, idsA, "A should be empty after purge")
|
||||
idsB, err = c.MessagesTo(context.Background(), toB)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(idsB), 1, "B should still have its message")
|
||||
|
||||
// Cleanup B
|
||||
require.NoError(t, c.PurgeMessagesTo(context.Background(), toB))
|
||||
}
|
||||
Reference in New Issue
Block a user