Stage 7 · Master
Phase 11 — Notice Service
Testing
Prove the notice module resists boundary-time bugs, ranking regressions, and live-feed duplication with focused Go tests.
How do you freeze the scheduled boundary in tests?
Boundary bugs hide in clock-dependent code because humans tend to test obvious moments like 'one minute later' rather than 'one second before'. The publisher already depends on a TimeSource, so tests can pin that source to exact timestamps and assert that a notice at 08:59:59 stays scheduled while the same row at 09:00:00 becomes published.
Because publication batches are org-scoped, the test should also prove one organization's due notice does not accidentally cause another organization's future-dated notice to flip. That is a business correctness assertion and a tenant-isolation assertion at the same time.
package notice
import (
"context"
"io"
"testing"
"time"
"github.com/google/uuid"
"github.com/rs/zerolog"
)
type fixedTimeSource struct {
value time.Time
}
func (f fixedTimeSource) Now(context.Context) (time.Time, error) {
return f.value, nil
}
type fakePublisherStore struct {
leader bool
scheduled map[uuid.UUID][]Notice
published map[uuid.UUID][]Notice
}
func (s *fakePublisherStore) TryAcquireLeader(context.Context) (bool, error) {
return s.leader, nil
}
func (s *fakePublisherStore) OrganizationsWithDueNotices(_ context.Context, asOf time.Time, limit int) ([]uuid.UUID, error) {
orgs := make([]uuid.UUID, 0, len(s.scheduled))
for orgID, notices := range s.scheduled {
for _, n := range notices {
if n.ScheduledAt != nil && !n.ScheduledAt.After(asOf) {
orgs = append(orgs, orgID)
break
}
}
if len(orgs) == limit {
break
}
}
return orgs, nil
}
func (s *fakePublisherStore) PublishDueNotices(_ context.Context, orgID uuid.UUID, asOf time.Time, limit int) ([]Notice, error) {
due := make([]Notice, 0, limit)
remaining := make([]Notice, 0, len(s.scheduled[orgID]))
for _, n := range s.scheduled[orgID] {
if len(due) < limit && n.ScheduledAt != nil && !n.ScheduledAt.After(asOf) {
n.State = StatePublished
publishedAt := asOf
n.PublishedAt = &publishedAt
due = append(due, n)
continue
}
remaining = append(remaining, n)
}
s.scheduled[orgID] = remaining
s.published[orgID] = append(s.published[orgID], due...)
return due, nil
}
func TestPublisherPublishesAtScheduledBoundary(t *testing.T) {
orgA := uuid.New()
orgB := uuid.New()
boundary := time.Date(2026, 8, 2, 9, 0, 0, 0, time.UTC)
early := boundary.Add(-time.Second)
later := boundary.Add(time.Minute)
dueAtBoundary := Notice{ID: uuid.New(), OrgID: orgA, State: StateScheduled, ScheduledAt: &boundary}
futureOtherTenant := Notice{ID: uuid.New(), OrgID: orgB, State: StateScheduled, ScheduledAt: &later}
store := &fakePublisherStore{
leader: true,
scheduled: map[uuid.UUID][]Notice{
orgA: {dueAtBoundary},
orgB: {futureOtherTenant},
},
published: map[uuid.UUID][]Notice{},
}
logger := zerolog.New(io.Discard)
earlyPublisher := NewPublisher(store, fixedTimeSource{value: early}, logger)
if err := earlyPublisher.RunOnce(context.Background()); err != nil {
t.Fatalf("RunOnce before boundary returned error: %v", err)
}
if got := len(store.published[orgA]); got != 0 {
t.Fatalf("expected 0 published notices before boundary, got %d", got)
}
boundaryPublisher := NewPublisher(store, fixedTimeSource{value: boundary}, logger)
if err := boundaryPublisher.RunOnce(context.Background()); err != nil {
t.Fatalf("RunOnce at boundary returned error: %v", err)
}
if got := len(store.published[orgA]); got != 1 {
t.Fatalf("expected 1 published notice at boundary, got %d", got)
}
if got := len(store.published[orgB]); got != 0 {
t.Fatalf("expected other tenant to remain untouched, got %d publishes", got)
}
}The same test demonstrates both the exact scheduled_at boundary and the requirement that one organization's publish cycle not spill into another tenant's queue.
How do you assert search ordering instead of eyeballing it?
Search ranking is one of the easiest features to regress accidentally because a changed weight or decay constant can still 'look okay' in manual testing. A deterministic fixture set makes the expectation executable: given one stale exact-title match and one recent near-match, the combined score should put the intended result first every time.
How do you simulate a live insert between pages?
The pagination test should reproduce the exact class of bug OFFSET suffers from: fetch page 1, insert a new top-of-feed row, then fetch page 2. Keyset is correct only if page 2 contains neither duplicates from page 1 nor a skip past older rows that the resident has not seen yet.
package notice
import (
"math"
"sort"
"testing"
"time"
"github.com/google/uuid"
)
type rankedFixture struct {
ID uuid.UUID
PublishedAt time.Time
Lexical float64
}
func combinedScore(now time.Time, item rankedFixture) float64 {
ageSeconds := now.Sub(item.PublishedAt).Seconds()
if ageSeconds < 0 {
ageSeconds = 0
}
return item.Lexical*0.85 + 0.15*math.Exp(-ageSeconds/604800.0)
}
func TestSearchRankingPrefersFreshCloseMatchWhenScoreIsHigher(t *testing.T) {
reference := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
staleExact := rankedFixture{
ID: uuid.New(),
PublishedAt: reference.AddDate(-2, 0, 0),
Lexical: 0.92,
}
freshClose := rankedFixture{
ID: uuid.New(),
PublishedAt: reference.Add(-72 * time.Hour),
Lexical: 0.88,
}
fixtures := []rankedFixture{staleExact, freshClose}
sort.Slice(fixtures, func(i, j int) bool {
left := combinedScore(reference, fixtures[i])
right := combinedScore(reference, fixtures[j])
if left == right {
return fixtures[i].ID.String() > fixtures[j].ID.String()
}
return left > right
})
if fixtures[0].ID != freshClose.ID {
t.Fatalf("expected fresh close match to rank first, got %s", fixtures[0].ID)
}
}
func TestKeysetPaginationAvoidsDuplicateWhenNewNoticeArrivesBetweenPages(t *testing.T) {
base := time.Date(2026, 8, 2, 10, 0, 0, 0, time.UTC)
n5Time := base.Add(-5 * time.Minute)
n4Time := base.Add(-4 * time.Minute)
n3Time := base.Add(-3 * time.Minute)
n2Time := base.Add(-2 * time.Minute)
n1Time := base.Add(-1 * time.Minute)
n5 := Notice{ID: uuid.MustParse("00000000-0000-0000-0000-000000000005"), PublishedAt: &n5Time}
n4 := Notice{ID: uuid.MustParse("00000000-0000-0000-0000-000000000004"), PublishedAt: &n4Time}
n3 := Notice{ID: uuid.MustParse("00000000-0000-0000-0000-000000000003"), PublishedAt: &n3Time}
n2 := Notice{ID: uuid.MustParse("00000000-0000-0000-0000-000000000002"), PublishedAt: &n2Time}
n1 := Notice{ID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), PublishedAt: &n1Time}
page1 := []Notice{n1, n2}
cursor := NewPublishedCursor(page1[len(page1)-1])
n6Time := base
n6 := Notice{ID: uuid.MustParse("00000000-0000-0000-0000-000000000006"), PublishedAt: &n6Time}
liveFeed := []Notice{n6, n1, n2, n3, n4, n5}
page2 := make([]Notice, 0, 2)
for _, item := range liveFeed {
if item.PublishedAt.Before(*cursor.PublishedAt) || (item.PublishedAt.Equal(*cursor.PublishedAt) && item.ID.String() < cursor.ID.String()) {
page2 = append(page2, item)
}
if len(page2) == 2 {
break
}
}
if len(page2) != 2 {
t.Fatalf("expected 2 items on page 2, got %d", len(page2))
}
if page2[0].ID != n3.ID || page2[1].ID != n4.ID {
t.Fatalf("expected page 2 to contain n3,n4, got %s,%s", page2[0].ID, page2[1].ID)
}
if page2[0].ID == n2.ID || page2[1].ID == n2.ID {
t.Fatalf("page 2 duplicated tail item from page 1")
}
}One file holds both ranking and pagination tests because they both verify ordering contracts that can regress invisibly during refactors.
Which commands should stay in your notice test loop?
Run the smallest tests that cover the changed behavior. Publisher boundary tests, ranking tests, and pagination tests together give confidence without forcing a full suite every time. When a test fails, read it as a domain contract violation first: publishing early, leaking across tenants, or duplicating feed rows are product bugs, not just red CI output.
Applied exercise
Turn notice bugs into focused tests
You are handed three regressions from staging: one notice published a second early, one search result order flipped after a ranking tweak, and one resident saw the same notice on both page 1 and page 2 during a busy morning. Convert those bug reports into a maintainable test plan.
- Map each regression to the most targeted test function name and fixture shape.
- Specify which dependency should be faked or injected for each test: time source, ranking reference timestamp, or live-feed slice.
- Describe one assertion in each test that proves the bug is fixed and stays fixed.
- Add one tenant-isolation assertion to the publisher scenario so it covers both timing and org scoping.
Deliverable
A three-test plan with named fixtures, one key assertion per bug, and a note on why these tests are better than relying on manual endpoint checks alone.
Completion checks
- Each regression is assigned to a narrow test instead of one giant integration test.
- The plan uses dependency injection for time or ordering inputs where relevant.
- At least one assertion explicitly protects tenant isolation, not just happy-path correctness.
What makes the publisher boundary test more reliable than sleeping for one second in a real worker test?
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.