package store import ( "context" "database/sql" "errors" "testing" "github.com/jackc/pgx/v5/pgconn" ) func TestMapPostCreateError(t *testing.T) { t.Parallel() for _, code := range []string{"23503", "23505", "23514"} { err := mapPostCreateError(&pgconn.PgError{Code: code}) if !errors.Is(err, ErrInvalidPost) { t.Errorf("code %s error = %v, want ErrInvalidPost", code, err) } } original := &pgconn.PgError{Code: "08006"} if err := mapPostCreateError(original); !errors.Is(err, original) { t.Errorf("unexpected database error was replaced: %v", err) } } func TestMemoryPostLifecycle(t *testing.T) { t.Parallel() ctx := context.Background() mem := NewMemory() homeowner := &User{Username: "homeowner", PasswordHash: "hash", Role: RoleUser} plumber := &User{Username: "plumber", PasswordHash: "hash", Role: RoleAdmin} voter := &User{Username: "voter", PasswordHash: "hash", Role: RoleUser} for _, user := range []*User{homeowner, plumber, voter} { if err := mem.CreateUser(ctx, user); err != nil { t.Fatal(err) } } root := &Post{ ID: "root", AuthorID: homeowner.ID, Title: "Leaky sink", Body: "It drips.", City: "Oakland", PostDate: "2026-08-26", CreatedAt: "2026-08-26T08:00:00Z", } if err := mem.CreatePost(ctx, root); err != nil { t.Fatal(err) } rootID := root.ID later := &Post{ ID: "later", ParentID: &rootID, AuthorID: plumber.ID, Body: "Is it a single-handle faucet?", CreatedAt: "2026-08-26T09:00:00Z", } earlier := &Post{ ID: "earlier", ParentID: &rootID, AuthorID: plumber.ID, Body: "Can you share the model number?", CreatedAt: "2026-08-26T08:30:00Z", } if err := mem.CreatePost(ctx, later); err != nil { t.Fatal(err) } if err := mem.CreatePost(ctx, earlier); err != nil { t.Fatal(err) } laterID := later.ID nested := &Post{ ID: "nested", ParentID: &laterID, AuthorID: homeowner.ID, Body: "Yes, it is.", CreatedAt: "2026-08-26T09:30:00Z", } if err := mem.CreatePost(ctx, nested); err != nil { t.Fatal(err) } thread, err := mem.GetPostThread(ctx, root.ID) if err != nil { t.Fatal(err) } if thread.AuthorName != homeowner.Name || thread.AuthorRole != RoleUser { t.Fatalf("root author = %q %q", thread.AuthorName, thread.AuthorRole) } if len(thread.Replies) != 2 || thread.Replies[0].ID != earlier.ID || thread.Replies[1].ID != later.ID { t.Fatalf("root replies are not oldest-first: %+v", thread.Replies) } if len(thread.Replies[1].Replies) != 1 || thread.Replies[1].Replies[0].ID != nested.ID { t.Fatalf("nested reply missing: %+v", thread.Replies[1].Replies) } otherParent := earlier.ID nested.ParentID = &otherParent nested.AuthorID = voter.ID nested.Body = "Yes—one handle." if err := mem.UpdatePost(ctx, nested); err != nil { t.Fatal(err) } saved, err := mem.GetPost(ctx, nested.ID) if err != nil { t.Fatal(err) } if saved.ParentID == nil || *saved.ParentID != later.ID { t.Fatalf("update changed parent to %+v", saved.ParentID) } if saved.AuthorID != homeowner.ID { t.Fatalf("update changed author to %q", saved.AuthorID) } if saved.Body != "Yes—one handle." || saved.UpdatedAt == saved.CreatedAt { t.Fatalf("body update not applied: %+v", saved) } if err := mem.VotePost(ctx, voter.ID, root.ID, 1); err != nil { t.Fatal(err) } roots, err := mem.ListRootPosts(ctx, root.PostDate, voter.ID) if err != nil { t.Fatal(err) } if len(roots) != 1 || roots[0].ID != root.ID { t.Fatalf("root list = %+v", roots) } if roots[0].Score != 1 || roots[0].UserVote != 1 || !roots[0].Answered { t.Fatalf("root annotations = %+v", roots[0]) } if err := mem.VotePost(ctx, voter.ID, later.ID, 1); !errors.Is(err, ErrPostNotVotable) { t.Fatalf("reply vote error = %v", err) } } func TestMemoryPostValidation(t *testing.T) { t.Parallel() ctx := context.Background() mem := NewMemory() homeowner := &User{Username: "homeowner", PasswordHash: "hash", Role: RoleUser} if err := mem.CreateUser(ctx, homeowner); err != nil { t.Fatal(err) } tests := []struct { name string post *Post }{ { name: "root without title", post: &Post{AuthorID: homeowner.ID, Body: "Body"}, }, { name: "empty parent", post: &Post{ParentID: ptr(""), AuthorID: homeowner.ID, Body: "Body"}, }, { name: "missing parent", post: &Post{ParentID: ptr("missing"), AuthorID: homeowner.ID, Body: "Body"}, }, { name: "reply with root fields", post: &Post{ ParentID: ptr("missing"), AuthorID: homeowner.ID, Title: "Not allowed", Body: "Body", }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if err := mem.CreatePost(ctx, test.post); !errors.Is(err, ErrInvalidPost) { t.Fatalf("error = %v, want ErrInvalidPost", err) } }) } hidden := &Post{ ID: "hidden", AuthorID: homeowner.ID, Title: "Hidden", Body: "Body", PostDate: "2026-08-26", Hidden: true, } if err := mem.CreatePost(ctx, hidden); err != nil { t.Fatal(err) } if err := mem.VotePost(ctx, homeowner.ID, hidden.ID, 1); !errors.Is(err, ErrPostNotVotable) { t.Fatalf("hidden root vote error = %v", err) } if _, err := mem.GetPostThread(ctx, "missing"); !errors.Is(err, sql.ErrNoRows) { t.Fatalf("missing thread error = %v", err) } } func ptr(value string) *string { return &value }