package httpserver import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/tim/cairnquire/apps/server/internal/auth" "github.com/tim/cairnquire/apps/server/internal/collaboration" ) func loginTestUser(t *testing.T, server *apiTestServer) *http.Cookie { t.Helper() login := httptest.NewRecorder() server.handler.ServeHTTP(login, jsonRequest(http.MethodPost, "/api/auth/login/password", map[string]string{ "email": "editor@example.com", "password": "very secure password", })) if login.Code != http.StatusOK { t.Fatalf("login response = %d %q", login.Code, login.Body.String()) } return cookieByName(t, login.Result().Cookies(), auth.SessionCookieName) } func authenticatedJSONRequest(t *testing.T, method, target string, body any, session *http.Cookie) *http.Request { t.Helper() request := jsonRequest(method, target, body) request.AddCookie(session) return request } func TestDocumentLoadsCollaborationControlsAndWatchFlow(t *testing.T) { server := newAPITestServer(t) session := loginTestUser(t, server) pageRequest := httptest.NewRequest(http.MethodGet, "/docs/hello", nil) pageRequest.AddCookie(session) page := httptest.NewRecorder() server.handler.ServeHTTP(page, pageRequest) if page.Code != http.StatusOK { t.Fatalf("document response = %d %q", page.Code, page.Body.String()) } for _, expected := range [][]byte{ []byte(`/static/notification-bell.js`), []byte(`data-document-id="doc:hello"`), []byte(`data-document-watch`), []byte(`data-comments-history`), } { if !bytes.Contains(page.Body.Bytes(), expected) { t.Errorf("document page missing %q", expected) } } statusRequest := httptest.NewRequest(http.MethodGet, "/api/watch/status?document_id=doc%3Ahello", nil) statusRequest.AddCookie(session) status := httptest.NewRecorder() server.handler.ServeHTTP(status, statusRequest) if status.Code != http.StatusOK || !bytes.Contains(status.Body.Bytes(), []byte(`"watching":false`)) { t.Fatalf("initial watch status = %d %q", status.Code, status.Body.String()) } watch := httptest.NewRecorder() server.handler.ServeHTTP(watch, authenticatedJSONRequest(t, http.MethodPost, "/api/watch/document", map[string]string{"documentId": "doc:hello"}, session)) if watch.Code != http.StatusOK { t.Fatalf("watch response = %d %q", watch.Code, watch.Body.String()) } status = httptest.NewRecorder() server.handler.ServeHTTP(status, statusRequest.Clone(statusRequest.Context())) if status.Code != http.StatusOK || !bytes.Contains(status.Body.Bytes(), []byte(`"watching":true`)) { t.Fatalf("watched status = %d %q", status.Code, status.Body.String()) } unwatch := httptest.NewRecorder() server.handler.ServeHTTP(unwatch, authenticatedJSONRequest(t, http.MethodDelete, "/api/watch/document", map[string]string{"documentId": "doc:hello"}, session)) if unwatch.Code != http.StatusOK { t.Fatalf("unwatch response = %d %q", unwatch.Code, unwatch.Body.String()) } } func TestCommentReplyResolveAndHistoryHTTPFlow(t *testing.T) { server := newAPITestServer(t) session := loginTestUser(t, server) anchor := "section-anchor" create := func(content string, parentID *string) collaboration.Comment { t.Helper() response := httptest.NewRecorder() server.handler.ServeHTTP(response, authenticatedJSONRequest(t, http.MethodPost, "/api/comments", collaboration.CreateCommentInput{ DocumentID: "doc:hello", VersionHash: "hash", ParentID: parentID, Content: content, AnchorHash: &anchor, }, session)) if response.Code != http.StatusCreated { t.Fatalf("create comment response = %d %q", response.Code, response.Body.String()) } var comment collaboration.Comment if err := json.NewDecoder(response.Body).Decode(&comment); err != nil { t.Fatalf("decode comment: %v", err) } return comment } root := create("Root", nil) reply := create("Reply", &root.ID) _ = create("Grandchild", &reply.ID) resolve := httptest.NewRecorder() server.handler.ServeHTTP(resolve, authenticatedJSONRequest(t, http.MethodPost, "/api/comments/"+root.ID+"/resolve", map[string]any{}, session)) if resolve.Code != http.StatusOK { t.Fatalf("resolve response = %d %q", resolve.Code, resolve.Body.String()) } list := func(includeResolved bool) []collaboration.Comment { t.Helper() target := "/api/comments?document_id=doc%3Ahello&anchor_hash=" + anchor if includeResolved { target += "&include_resolved=true" } request := httptest.NewRequest(http.MethodGet, target, nil) request.AddCookie(session) response := httptest.NewRecorder() server.handler.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("list comments response = %d %q", response.Code, response.Body.String()) } var body struct { Comments []collaboration.Comment `json:"comments"` } if err := json.NewDecoder(response.Body).Decode(&body); err != nil { t.Fatalf("decode comments: %v", err) } return body.Comments } if active := list(false); len(active) != 0 { t.Fatalf("active comments = %d, want 0", len(active)) } if history := list(true); len(history) != 3 { t.Fatalf("history comments = %d, want 3", len(history)) } }