#11: implement MTA-STS, TLS-RPT & Autoconfig HTTP services
This commit is contained in:
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user