diff --git a/docs/plans/delivery-log-storage.md b/docs/plans/delivery-log-storage.md index 1e63e91..92beec0 100644 --- a/docs/plans/delivery-log-storage.md +++ b/docs/plans/delivery-log-storage.md @@ -147,6 +147,6 @@ Target version cut: **`2.2.0`** (MINOR). One commit per step; - [x] The two texts, mockup first: the *Outbound log* help topic and the empty note (`design:` commit) — **Opus** (the help topic is in the mockups; the empty note is not drawn there - the `out-message` mockup shows a message with lines - and changes with the page) - [x] Message page reads the table, falls back to the current `mail.log`; help text in `help.go` — **Opus** - [x] [architecture.md](../architecture.md), [guide.md](../guide.md), [schema-migrations.md](../schema-migrations.md) — **Opus** (schema-migrations.md went in with the migration) -- [ ] `go vet`, `go test`, e2e: a message's page still lists its lines after a forced rotation — **Opus** +- [x] `go vet`, `go test`, e2e: a message's page still lists its lines after a forced rotation — **Opus** (on the test server: vet ok, 22 packages ok, e2e ok with the new step `delivery_log_survives_rotation`) - [ ] Technical review — **Opus** - [ ] Security review — **Fable** diff --git a/docs/roadmap.md b/docs/roadmap.md index 59ecd99..268e7b4 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -36,7 +36,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md). | structured-logging | `log/slog` with levels and fields | candidate | — | — | | review-2026-08-followups | Remaining findings of the 2026-08-19 code review | candidate | — | — | | panel-notifications | E-mail notifications about important events | candidate | — | — | -| delivery-log-storage | Keep a message's `mail.log` lines with its send-log row | in progress | 5/8 | [plans/delivery-log-storage.md](plans/delivery-log-storage.md) | +| delivery-log-storage | Keep a message's `mail.log` lines with its send-log row | in progress | 6/8 | [plans/delivery-log-storage.md](plans/delivery-log-storage.md) | | password-reset | Password reset by e-mail | candidate | — | — | | phone-desktop-mode | A phone browser in "desktop site" mode gets the phone navigation | candidate | — | — | | panel-redesign | Panel redesign (accepted mockups, design contract) | shipped `2.0.0` | 27/27 | [plans/panel-redesign.md](plans/panel-redesign.md) | diff --git a/test/e2e/delivery_log_test.go b/test/e2e/delivery_log_test.go new file mode 100644 index 0000000..b3ad200 --- /dev/null +++ b/test/e2e/delivery_log_test.go @@ -0,0 +1,68 @@ +package e2e + +import ( + "fmt" + "html" + "regexp" + "strings" + "testing" + "time" +) + +var sendLogDetailsRe = regexp.MustCompile(`href="(/outbound/log/\d+[^"]*)"`) + +// firstDeliveryPage returns the address of the page of the newest row in the +// sender domain's send log. +func firstDeliveryPage(sc *scenario) (string, error) { + rows, err := sc.panel.sendLogRows(senderDomain, "") + if err != nil { + return "", fmt.Errorf("read the send log: %w", err) + } + m := sendLogDetailsRe.FindStringSubmatch(rows) + if m == nil { + return "", fmt.Errorf("no Details link in the send log:\n%s", rows) + } + return html.UnescapeString(m[1]), nil +} + +// testDeliveryLogSurvivesRotation checks that a message's page still lists +// what Postfix wrote about it after mail.log has been rotated: the lines are +// kept in the database by the log tailer, not searched for in the current +// file (architecture.md § Log tailer, Stored lines). The message is the first +// one delivered, whose page send_verify_dkim_and_status noted. +func testDeliveryLogSurvivesRotation(t *testing.T, sc *scenario) { + page := sc.deliveryPage + + // What the page has to show: the line that opened the queue id and the + // delivery line. The tailer has read both - the row is "sent" - but it + // writes them one at a time, so the page is polled rather than read once. + wanted := []string{"client=", "status=sent"} + shows := func() (bool, error) { + _, body, err := sc.panel.get(page) + if err != nil { + return false, err + } + for _, w := range wanted { + if !strings.Contains(body, w) { + return false, fmt.Errorf("%s does not show a %q line", page, w) + } + } + return true, nil + } + if err := waitFor("the message's page to list its log lines", 15*time.Second, 500*time.Millisecond, shows); err != nil { + t.Fatal(err) + } + + if _, err := h.execIn("selfpost", "logrotate", "-f", "/etc/logrotate.d/mail"); err != nil { + t.Fatalf("logrotate -f: %v", err) + } + // The current file must no longer hold the delivery line, or the page + // finding it there would prove nothing. grep exits 1 on no match. + if out, err := h.execIn("selfpost", "sh", "-c", "grep -c 'status=sent' /data/log/mail.log || true"); err != nil || strings.TrimSpace(out) != "0" { + t.Fatalf("mail.log still holds delivery lines after the rotation (count=%q err=%v)", strings.TrimSpace(out), err) + } + + if ok, err := shows(); !ok { + t.Fatalf("after mail.log was rotated: %v", err) + } +} diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go index d8f0623..a8c0071 100644 --- a/test/e2e/main_test.go +++ b/test/e2e/main_test.go @@ -92,6 +92,8 @@ type scenario struct { l2AppPassword string clientIP string zoneRecords []txtRecord + // deliveryPage is the panel page of the first delivered message. + deliveryPage string } func TestE2E(t *testing.T) { @@ -251,6 +253,14 @@ func TestE2E(t *testing.T) { }); err != nil { t.Fatal(err) } + + // The only row so far; its page is looked at again once the log has + // been rotated (delivery_log_survives_rotation). + page, err := firstDeliveryPage(sc) + if err != nil { + t.Fatal(err) + } + sc.deliveryPage = page }) run("negative_level2_ratelimit_via_panel", func(t *testing.T) { @@ -274,6 +284,11 @@ func TestE2E(t *testing.T) { run("negative_level1_ratelimit", func(t *testing.T) { testLevel1RateLimit(t, sc) }) + // Rotates mail.log, so it comes after the steps that read what they need + // from the current file. + run("delivery_log_survives_rotation", func(t *testing.T) { + testDeliveryLogSurvivesRotation(t, sc) + }) // From here on the installation is no longer the one the run started: it // is restored from its own backup, then switched to accept mail on port 25.