package web import ( "context" "encoding/json" "net/http" "net/url" "strings" "testing" "plumber/internal/events" "plumber/internal/pacific" "plumber/internal/store" ) func TestPostHandlersPublishEvents(t *testing.T) { t.Parallel() rec := &events.Recording{} srv, mem := newTestServer(t, Config{ Events: rec, BaseURL: "https://www.askaplumberfirst.com", }) handler := srv.Handler() homeowner := seedUser(t, mem, uniq("homeowner"), "hunter22", store.RoleUser) admin := seedUser(t, mem, uniq("admin"), "hunter22", store.RoleAdmin) homeownerCookies := loginUser(t, handler, homeowner.Username, "hunter22") adminCookies := loginUser(t, handler, admin.Username, "hunter22") homeownerCSRF := csrfForCookies(t, handler, homeownerCookies) adminCSRF := csrfForCookies(t, handler, adminCookies) submit := postForm(handler, "/submit", url.Values{ "_csrf": {homeownerCSRF}, "title": {"Leaky sink"}, "body": {"Water under the cabinet."}, "city": {"Oakland"}, }, homeownerCookies) if submit.Code != http.StatusSeeOther { t.Fatalf("submit status = %d: %s", submit.Code, submit.Body.String()) } create := postForm(handler, "/posts", url.Values{ "_csrf": {homeownerCSRF}, "title": {"Second question"}, "body": {"Another leak."}, "city": {"Berkeley"}, }, homeownerCookies) if create.Code != http.StatusSeeOther { t.Fatalf("create status = %d: %s", create.Code, create.Body.String()) } roots, err := mem.ListRootPosts(context.Background(), pacific.Today(), homeowner.ID) if err != nil || len(roots) != 2 { t.Fatalf("roots = %+v, %v", roots, err) } var submitRoot, createRoot store.Post for _, root := range roots { switch root.Title { case "Leaky sink": submitRoot = root case "Second question": createRoot = root } } if submitRoot.ID == "" || createRoot.ID == "" { t.Fatalf("missing created roots: %+v", roots) } reply := postForm(handler, "/posts", url.Values{ "_csrf": {adminCSRF}, "parent_id": {createRoot.ID}, "body": {"Replace the cartridge."}, }, adminCookies) if reply.Code != http.StatusSeeOther { t.Fatalf("reply status = %d: %s", reply.Code, reply.Body.String()) } thread, err := mem.GetPostThread(context.Background(), createRoot.ID) if err != nil || len(thread.Replies) != 1 { t.Fatalf("thread = %+v, %v", thread, err) } adminReply := thread.Replies[0] edit := postForm(handler, "/posts/"+createRoot.ID+"/edit", url.Values{ "_csrf": {homeownerCSRF}, "body": {"Updated leak description."}, }, homeownerCookies) if edit.Code != http.StatusSeeOther { t.Fatalf("edit status = %d: %s", edit.Code, edit.Body.String()) } empty := postForm(handler, "/posts", url.Values{ "_csrf": {homeownerCSRF}, "title": {"Missing body"}, }, homeownerCookies) if empty.Code != http.StatusBadRequest { t.Fatalf("empty body status = %d, want 400", empty.Code) } hidden := &store.Post{ AuthorID: homeowner.ID, Title: "Hidden thread", Body: "Not public.", PostDate: pacific.Today(), PostState: store.PostStateHidden, } if err := mem.CreatePost(context.Background(), hidden); err != nil { t.Fatal(err) } hiddenReply := postForm(handler, "/posts", url.Values{ "_csrf": {homeownerCSRF}, "parent_id": {hidden.ID}, "body": {"Should not publish."}, }, homeownerCookies) if hiddenReply.Code != http.StatusNotFound { t.Fatalf("hidden reply status = %d, want 404", hiddenReply.Code) } hiddenEdit := postForm(handler, "/posts/"+hidden.ID+"/edit", url.Values{ "_csrf": {homeownerCSRF}, "body": {"Still hidden."}, }, homeownerCookies) if hiddenEdit.Code != http.StatusSeeOther { t.Fatalf("hidden edit status = %d: %s", hiddenEdit.Code, hiddenEdit.Body.String()) } got := rec.Snapshot() if len(got) != 4 { t.Fatalf("published %d events, want 4: %#v", len(got), got) } submitEv, ok := got[0].(events.PostCreated) if !ok { t.Fatalf("first event %T, want PostCreated", got[0]) } assertPostEvent(t, submitEv.PostEvent, events.PostEvent{ PostID: submitRoot.ID, RootID: submitRoot.ID, Title: "Leaky sink", Body: "Water under the cabinet.", City: "Oakland", AuthorID: homeowner.ID, AuthorName: homeowner.Name, AuthorRole: string(store.RoleUser), Permalink: "https://www.askaplumberfirst.com/questions/" + submitRoot.ID + "#post-" + submitRoot.ID, }) createEv, ok := got[1].(events.PostCreated) if !ok { t.Fatalf("second event %T, want PostCreated", got[1]) } assertPostEvent(t, createEv.PostEvent, events.PostEvent{ PostID: createRoot.ID, RootID: createRoot.ID, Title: "Second question", Body: "Another leak.", City: "Berkeley", AuthorID: homeowner.ID, AuthorName: homeowner.Name, AuthorRole: string(store.RoleUser), Permalink: "https://www.askaplumberfirst.com/questions/" + createRoot.ID + "#post-" + createRoot.ID, }) replyEv, ok := got[2].(events.PostCreated) if !ok { t.Fatalf("third event %T, want PostCreated", got[2]) } assertPostEvent(t, replyEv.PostEvent, events.PostEvent{ PostID: adminReply.ID, RootID: createRoot.ID, ParentID: createRoot.ID, Body: "Replace the cartridge.", AuthorID: admin.ID, AuthorName: admin.Name, AuthorRole: string(store.RoleAdmin), Permalink: "https://www.askaplumberfirst.com/questions/" + createRoot.ID + "#post-" + adminReply.ID, }) editEv, ok := got[3].(events.PostUpdated) if !ok { t.Fatalf("fourth event %T, want PostUpdated", got[3]) } assertPostEvent(t, editEv.PostEvent, events.PostEvent{ PostID: createRoot.ID, RootID: createRoot.ID, Title: "Second question", Body: "Updated leak description.", City: "Berkeley", AuthorID: homeowner.ID, AuthorName: homeowner.Name, AuthorRole: string(store.RoleUser), Permalink: "https://www.askaplumberfirst.com/questions/" + createRoot.ID + "#post-" + createRoot.ID, }) for i, ev := range got { raw, err := json.Marshal(ev) if err != nil { t.Fatal(err) } if strings.Contains(strings.ToLower(string(raw)), "discord") { t.Fatalf("event %d contains discord fields: %s", i, raw) } } } func TestStoreCreateDoesNotPublish(t *testing.T) { t.Parallel() rec := &events.Recording{} _, mem := newTestServer(t, Config{Events: rec}) homeowner := seedUser(t, mem, uniq("homeowner"), "hunter22", store.RoleUser) if err := mem.CreatePost(context.Background(), &store.Post{ AuthorID: homeowner.ID, Title: "Direct write", Body: "No handler.", PostDate: pacific.Today(), }); err != nil { t.Fatal(err) } if rec.Len() != 0 { t.Fatalf("store.CreatePost published %d events", rec.Len()) } } func assertPostEvent(t *testing.T, got, want events.PostEvent) { t.Helper() if got.PostID != want.PostID || got.RootID != want.RootID || got.ParentID != want.ParentID || got.Title != want.Title || got.Body != want.Body || got.City != want.City || got.AuthorID != want.AuthorID || got.AuthorName != want.AuthorName || got.AuthorRole != want.AuthorRole || got.Permalink != want.Permalink || len(got.Images) != 0 { t.Fatalf("event = %+v, want %+v", got, want) } }