#11: implement MTA-STS, TLS-RPT & Autoconfig HTTP services

This commit is contained in:
Lucas Rayan committed 2026-07-25 10:18:03 -03:00
1 parent c0042ef207
commit 314eb67dda
20 files changed
+1177 -31

No files matched your search

@@ -0,0 +1,145 @@
package e2e
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
appusecase "lambdamail/protocols/internal/application/usecase"
"lambdamail/protocols/internal/infrastructure/postgres"
httppresentation "lambdamail/protocols/internal/presentation/http"
)
func TestHttpServicesEndToEnd(t *testing.T) {
if testing.Short() {
t.Skip("e2e test skipped in -short mode")
}
ctx := context.Background()
runtimeDir := t.TempDir()
pg := embeddedpostgres.NewDatabase(embeddedpostgres.DefaultConfig().
Port(54337).
RuntimePath(runtimeDir).
StartTimeout(120 * time.Second))
if err := pg.Start(); err != nil {
t.Fatalf("start embedded postgres: %v", err)
}
defer pg.Stop()
dbURL := "postgres://postgres:postgres@localhost:54337/postgres?sslmode=disable"
pool, err := postgres.NewPool(ctx, dbURL)
if err != nil {
t.Fatalf("connect pool: %v", err)
}
defer pool.Close()
root := repoRoot(t)
migrations := []string{
"0001_init_schema.up.sql",
"0002_add_is_system_to_aliases.up.sql",
"0003_create_report_tables.up.sql",
}
for _, m := range migrations {
sql, err := os.ReadFile(filepath.Join(root, "migrations", m))
if err != nil {
t.Fatalf("read migration %s: %v", m, err)
}
if _, err := pool.Exec(ctx, string(sql)); err != nil {
t.Fatalf("apply migration %s: %v", m, err)
}
}
reportRepo := postgres.NewReportRepository(pool)
reportUseCase := appusecase.NewIngestReportsUseCase(reportRepo)
router := httppresentation.NewRouter(reportUseCase, func() error { return pool.Ping(ctx) })
ts := httptest.NewServer(router)
defer ts.Close()
client := ts.Client()
// 1. MTA-STS policy check
req, _ := http.NewRequest(http.MethodGet, ts.URL+"/.well-known/mta-sts.txt", nil)
req.Host = "mta-sts.httpdomain.test"
resp, err := client.Do(req)
if err != nil || resp.StatusCode != http.StatusOK {
t.Fatalf("mta-sts request failed: err=%v, code=%d", err, resp.StatusCode)
}
bodyBytes, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if !strings.Contains(string(bodyBytes), "version: STSv1") || !strings.Contains(string(bodyBytes), "mx: mail.httpdomain.test") {
t.Errorf("mta-sts policy mismatch: %s", string(bodyBytes))
}
// 2. Thunderbird autoconfig XML check
req, _ = http.NewRequest(http.MethodGet, ts.URL+"/mail/config-v1.1.xml", nil)
req.Host = "autoconfig.httpdomain.test"
resp, err = client.Do(req)
if err != nil || resp.StatusCode != http.StatusOK {
t.Fatalf("autoconfig request failed: err=%v, code=%d", err, resp.StatusCode)
}
bodyBytes, _ = io.ReadAll(resp.Body)
resp.Body.Close()
if !strings.Contains(string(bodyBytes), "<clientConfig version=\"1.1\">") {
t.Errorf("autoconfig xml mismatch: %s", string(bodyBytes))
}
// 3. Security.txt check
req, _ = http.NewRequest(http.MethodGet, ts.URL+"/.well-known/security.txt", nil)
req.Host = "httpdomain.test"
resp, err = client.Do(req)
if err != nil || resp.StatusCode != http.StatusOK {
t.Fatalf("security.txt request failed: err=%v, code=%d", err, resp.StatusCode)
}
bodyBytes, _ = io.ReadAll(resp.Body)
resp.Body.Close()
if !strings.Contains(string(bodyBytes), "Contact: mailto:security@httpdomain.test") {
t.Errorf("security.txt mismatch: %s", string(bodyBytes))
}
// 4. Ingest DMARC report over HTTP and verify PostgreSQL persistence
dmarcPayload := `<?xml version="1.0"?>
<feedback>
<report_metadata><org_name>httporg.com</org_name><report_id>e2e123</report_id><date_range><begin>1600000000</begin><end>1600086400</end></date_range></report_metadata>
<policy_published><domain>httpdomain.test</domain></policy_published>
<record><row><source_ip>198.51.100.5</source_ip><count>12</count><policy_evaluated><disposition>none</disposition><dkim>pass</dkim><spf>pass</spf></policy_evaluated></row><identifiers><header_from>httpdomain.test</header_from></identifiers></record>
</feedback>`
resp, err = client.Post(ts.URL+"/api/v1/reports/dmarc", "application/xml", bytes.NewBufferString(dmarcPayload))
if err != nil || resp.StatusCode != http.StatusOK {
t.Fatalf("dmarc ingest failed: err=%v, code=%d", err, resp.StatusCode)
}
var dmarcCount int
if err := pool.QueryRow(ctx, `SELECT count(*) FROM dmarc_reports WHERE org_name = 'httporg.com'`).Scan(&dmarcCount); err != nil || dmarcCount != 1 {
t.Fatalf("expected 1 dmarc report in DB, got count=%d, err=%v", dmarcCount, err)
}
// 5. Ingest TLS-RPT report over HTTP and verify PostgreSQL persistence
tlsRptPayload := `{
"organization-name": "httporg.com",
"report-id": "tls123",
"date-range": {"start-datetime": "2026-07-25T00:00:00Z", "end-datetime": "2026-07-25T23:59:59Z"},
"policies": [{"policy": {"policy-type": "sts", "domain": "httpdomain.test"}, "summary": {"total-successful-session-count": 50, "total-failure-session-count": 0}}]
}`
resp, err = client.Post(ts.URL+"/api/v1/reports/tlsrpt", "application/json", bytes.NewBufferString(tlsRptPayload))
if err != nil || resp.StatusCode != http.StatusOK {
t.Fatalf("tls-rpt ingest failed: err=%v, code=%d", err, resp.StatusCode)
}
var tlsRptCount int
if err := pool.QueryRow(ctx, `SELECT count(*) FROM tls_rpt_reports WHERE organization_name = 'httporg.com'`).Scan(&tlsRptCount); err != nil || tlsRptCount != 1 {
t.Fatalf("expected 1 tls-rpt report in DB, got count=%d, err=%v", tlsRptCount, err)
}
}