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), "") { 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 := ` httporg.come2e12316000000001600086400 httpdomain.test 198.51.100.512nonepasspasshttpdomain.test ` 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) } }