diff --git a/mutations_add_comment_test.go b/mutations_add_comment_test.go new file mode 100644 index 0000000..ba5e608 --- /dev/null +++ b/mutations_add_comment_test.go @@ -0,0 +1,77 @@ +package main + +import ( + "context" + "sync" + "testing" + "time" +) + +// TestConcurrentComments_AllRetained — 100 goroutines add comments to the +// same post. All should land, no path collisions, no lost writes. +func TestConcurrentComments_AllRetained(t *testing.T) { + s, _, _ := newTestStore(t) + cp := &CreatePost{AuthorID: "scott", Body: "race test"} + must(t, Submit(context.Background(), s, cp)) + + const N = 100 + var wg sync.WaitGroup + errCh := make(chan error, N) + for i := 0; i < N; i++ { + wg.Add(1) + go func() { + defer wg.Done() + m := &AddComment{PostID: cp.ResolvedPostID(), AuthorID: "hermes", Body: "concurrent"} + errCh <- Submit(context.Background(), s, m) + }() + } + wg.Wait() + close(errCh) + for err := range errCh { + if err != nil { + t.Fatalf("concurrent submit: %v", err) + } + } + comments := s.ListComments(cp.ResolvedPostID()) + if len(comments) != N { + t.Fatalf("expected %d comments, got %d", N, len(comments)) + } + // All paths must be unique and parseable. + seen := make(map[string]bool, N) + for _, c := range comments { + if seen[c.Path] { + t.Fatalf("duplicate path: %s", c.Path) + } + seen[c.Path] = true + } +} + +// TestBumpOrder_AfterConcurrentComments — concurrent comments land in some +// order, but the final post bump_at should be the time of the LAST comment. +// With the per-author coarse lock, mutations serialize, so the order is +// deterministic given the goroutine schedule. We don't assert exact order +// (that would be flaky); we assert that the final bump_at is the latest. +func TestBumpOrder_AfterConcurrentComments(t *testing.T) { + s, clock, _ := newTestStore(t) + cp := &CreatePost{AuthorID: "scott", Body: "bump order"} + must(t, Submit(context.Background(), s, cp)) + initialBump := s.posts[cp.ResolvedPostID()].BumpedAt + + const N = 10 + var wg sync.WaitGroup + for i := 0; i < N; i++ { + wg.Add(1) + go func() { + defer wg.Done() + clock.Advance(time.Second) + m := &AddComment{PostID: cp.ResolvedPostID(), AuthorID: "hermes", Body: "x"} + _ = Submit(context.Background(), s, m) + }() + } + wg.Wait() + + got, _ := s.GetPost(cp.ResolvedPostID()) + if !got.BumpedAt.After(initialBump) { + t.Fatalf("post should have been bumped past %v, got %v", initialBump, got.BumpedAt) + } +}