package store import ( "context" "testing" "time" ) // seedBodyMessage stores one message with a body and a chosen received_at. func seedBodyMessage(t *testing.T, s *Store, ctx context.Context, gmailID, body string, receivedAt time.Time) int64 { t.Helper() id, err := s.UpsertMessage(ctx, &Message{ GmailID: gmailID, FromAddr: "sender@example.test", ToAddr: "hello@where-woof.com", Subject: "Subject " + gmailID, BodyText: body, SourceAddress: "hello@where-woof.com", DateSent: receivedAt, ReceivedAt: receivedAt, }) if err != nil { t.Fatalf("seed body message: %v", err) } return id } // bodyOf reads body_text directly, distinguishing NULL from empty. func bodyOf(t *testing.T, s *Store, ctx context.Context, id int64) (string, bool) { t.Helper() var body *string if err := s.DB().QueryRowContext(ctx, `SELECT body_text FROM messages WHERE id = ?`, id).Scan(&body); err != nil { t.Fatalf("read body of message %d: %v", id, err) } if body == nil { return "", true } return *body, false } func TestRetainClearsOldBodiesKeepsMetadata(t *testing.T) { s, ctx := testStore(t) old := seedBodyMessage(t, s, ctx, "old-1", "an old body", time.Now().UTC().Add(-45*24*time.Hour)) recent := seedBodyMessage(t, s, ctx, "recent-1", "a recent body", time.Now().UTC().Add(-2*24*time.Hour)) res, err := s.Retain(ctx, RetentionPolicy{BodyMaxAge: 30 * 24 * time.Hour}) if err != nil { t.Fatalf("Retain() error = %v", err) } if res.BodiesCleared != 1 { t.Errorf("BodiesCleared = %d, want 1", res.BodiesCleared) } if body, null := bodyOf(t, s, ctx, old); !null || body != "" { t.Errorf("old body = %q (null=%v), want NULL", body, null) } if body, null := bodyOf(t, s, ctx, recent); null || body != "a recent body" { t.Errorf("recent body = %q (null=%v), want unchanged", body, null) } // Metadata must survive the body clear. m, err := s.MessageByID(ctx, old) if err != nil { t.Fatalf("MessageByID() error = %v", err) } if m.Subject != "Subject old-1" || m.SourceAddress != "hello@where-woof.com" { t.Errorf("metadata lost after retention: %+v", m) } if m.BodyText != "" { t.Errorf("MessageByID().BodyText = %q, want empty", m.BodyText) } // A second pass is a no-op: the row is already NULL, not an empty string. res2, err := s.Retain(ctx, RetentionPolicy{BodyMaxAge: 30 * 24 * time.Hour}) if err != nil { t.Fatalf("second Retain() error = %v", err) } if res2.BodiesCleared != 0 { t.Errorf("second BodiesCleared = %d, want 0", res2.BodiesCleared) } } func TestRetainCapsActionsLog(t *testing.T) { s, ctx := testStore(t) for i := 0; i < 5; i++ { if err := s.LogAction(ctx, "fetch", "scheduler", "pass"); err != nil { t.Fatalf("LogAction() %d error = %v", i, err) } } res, err := s.Retain(ctx, RetentionPolicy{ActionsLogMaxRows: 2}) if err != nil { t.Fatalf("Retain() error = %v", err) } if res.ActionsDeleted != 3 { t.Errorf("ActionsDeleted = %d, want 3", res.ActionsDeleted) } entries, err := s.RecentActions(ctx, 100) if err != nil { t.Fatalf("RecentActions() error = %v", err) } if len(entries) != 2 { t.Fatalf("actions_log rows = %d, want 2", len(entries)) } // RecentActions is newest-first; the two kept rows are the newest inserts. if entries[0].ID <= entries[1].ID { t.Errorf("kept rows not the newest: %d, %d", entries[0].ID, entries[1].ID) } // A smaller table than the cap is untouched. res2, err := s.Retain(ctx, RetentionPolicy{ActionsLogMaxRows: 50}) if err != nil { t.Fatalf("second Retain() error = %v", err) } if res2.ActionsDeleted != 0 { t.Errorf("second ActionsDeleted = %d, want 0", res2.ActionsDeleted) } } func TestRetainDisabledPolicyIsNoOp(t *testing.T) { s, ctx := testStore(t) old := seedBodyMessage(t, s, ctx, "old-1", "an old body", time.Now().UTC().Add(-400*24*time.Hour)) if err := s.LogAction(ctx, "fetch", "scheduler", "pass"); err != nil { t.Fatalf("LogAction() error = %v", err) } res, err := s.Retain(ctx, RetentionPolicy{}) if err != nil { t.Fatalf("Retain() error = %v", err) } if res.BodiesCleared != 0 || res.ActionsDeleted != 0 { t.Errorf("disabled policy changed data: %+v", res) } if body, null := bodyOf(t, s, ctx, old); null || body != "an old body" { t.Errorf("old body = %q (null=%v), want unchanged", body, null) } }