package api import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // commentJSON builds a single comment node as a generic map. Helpers below // compose these into a paginated list response. type commentJSON map[string]any func footerComment(id, body, displayName string, replies ...commentJSON) commentJSON { return commentJSON{ "id": id, "type": "comment", "status": "current", "body": map[string]any{ "storage": map[string]any{ "value": body, "representation": "storage", }, }, "version": map[string]any{ "number": 1, "when": "2025-12-02T09:00:00.000Z", }, "history": map[string]any{ "createdDate": "2025-12-02T09:00:00.000Z", "createdBy": map[string]any{ "username": "u-" + id, "displayName": displayName, "userKey": "key-" + id, }, }, "extensions": map[string]any{ "location": "footer", }, "children": childrenWrap(replies), } } func inlineComment(id, body, markerRef, resolution, displayName string, replies ...commentJSON) commentJSON { ext := map[string]any{ "location": "inline", "inlineProperties": map[string]any{ "markerRef": markerRef, }, } if resolution != "" { ext["resolution"] = map[string]any{"status": resolution} } return commentJSON{ "id": id, "type": "comment", "status": "current", "body": map[string]any{ "storage": map[string]any{ "value": body, "representation": "storage", }, }, "version": map[string]any{ "number": 1, "when": "2025-12-01T11:00:00.000Z", }, "history": map[string]any{ "createdDate": "2025-12-01T10:15:00.000Z", "createdBy": map[string]any{ "username": "u-" + id, "displayName": displayName, }, }, "extensions": ext, "children": childrenWrap(replies), } } func childrenWrap(replies []commentJSON) map[string]any { results := make([]any, 0, len(replies)) for _, r := range replies { results = append(results, r) } return map[string]any{ "comment": map[string]any{ "results": results, "size": len(results), }, } } func listResponse(results []commentJSON, next string) map[string]any { out := make([]any, 0, len(results)) for _, r := range results { out = append(out, r) } resp := map[string]any{ "results": out, "size": len(out), "limit": 200, "start": 0, "_links": map[string]any{}, } if next != "" { resp["_links"] = map[string]any{"next": next} } return resp } func writeJSON(t *testing.T, w http.ResponseWriter, payload any) { t.Helper() w.Header().Set("Content-Type", "application/json") require.NoError(t, json.NewEncoder(w).Encode(payload)) } func TestGetComments_Empty(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/rest/api/content/12345/child/comment", r.URL.Path) writeJSON(t, w, listResponse(nil, "")) })) defer server.Close() client := NewClient(server.URL, "test-token") result, err := client.GetComments("12345") require.NoError(t, err) assert.Empty(t, result) } func TestGetComments_FooterOnly(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { writeJSON(t, w, listResponse([]commentJSON{ footerComment("100", "

LGTM

", "Bob"), footerComment("101", "

nice

", "Eve"), }, "")) })) defer server.Close() client := NewClient(server.URL, "test-token") result, err := client.GetComments("12345") require.NoError(t, err) require.Len(t, result, 2) assert.Equal(t, "footer", result[0].Location) assert.Empty(t, result[0].MarkerRef) assert.Equal(t, "

LGTM

", result[0].BodyStorage) assert.Equal(t, "Bob", result[0].Author.DisplayName) } func TestGetComments_InlineWithMarker(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { writeJSON(t, w, listResponse([]commentJSON{ inlineComment("200", "

clarify?

", "uuid-A", "open", "Jane"), }, "")) })) defer server.Close() client := NewClient(server.URL, "test-token") result, err := client.GetComments("12345") require.NoError(t, err) require.Len(t, result, 1) assert.Equal(t, "inline", result[0].Location) assert.Equal(t, "uuid-A", result[0].MarkerRef) assert.False(t, result[0].Resolved) assert.Equal(t, "open", result[0].Resolution) assert.Equal(t, "Jane", result[0].Author.DisplayName) } func TestGetComments_Resolved(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { writeJSON(t, w, listResponse([]commentJSON{ inlineComment("300", "

fixed

", "uuid-B", "resolved", "Jane"), }, "")) })) defer server.Close() client := NewClient(server.URL, "test-token") result, err := client.GetComments("12345") require.NoError(t, err) require.Len(t, result, 1) assert.True(t, result[0].Resolved) assert.Equal(t, "resolved", result[0].Resolution) } func TestGetComments_NestedReplies(t *testing.T) { grandchild := inlineComment("403", "

thanks

", "uuid-C", "open", "Jane") child1 := inlineComment("401", "

agreed

", "uuid-C", "open", "Alice", grandchild) child2 := inlineComment("402", "

also

", "uuid-C", "open", "Bob") parent := inlineComment("400", "

q?

", "uuid-C", "open", "Jane", child1, child2) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { writeJSON(t, w, listResponse([]commentJSON{parent}, "")) })) defer server.Close() client := NewClient(server.URL, "test-token") result, err := client.GetComments("12345") require.NoError(t, err) require.Len(t, result, 1) assert.Equal(t, "400", result[0].ID) require.Len(t, result[0].Replies, 2) assert.Equal(t, "401", result[0].Replies[0].ID) require.Len(t, result[0].Replies[0].Replies, 1) assert.Equal(t, "403", result[0].Replies[0].Replies[0].ID) assert.Equal(t, "402", result[0].Replies[1].ID) } func TestGetComments_PaginationRelative(t *testing.T) { var hits int32 var server *httptest.Server server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n := atomic.AddInt32(&hits, 1) switch n { case 1: writeJSON(t, w, listResponse([]commentJSON{ footerComment("1", "

a

", "A"), }, "/rest/api/content/12345/child/comment?start=1&limit=200")) case 2: assert.Equal(t, "1", r.URL.Query().Get("start")) writeJSON(t, w, listResponse([]commentJSON{ footerComment("2", "

b

", "B"), }, "")) default: t.Fatalf("unexpected request to %s", r.URL.String()) } _ = server })) defer server.Close() client := NewClient(server.URL, "test-token") result, err := client.GetComments("12345") require.NoError(t, err) require.Len(t, result, 2) assert.Equal(t, int32(2), atomic.LoadInt32(&hits)) } func TestGetComments_PaginationAbsolute(t *testing.T) { var hits int32 var serverURL string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n := atomic.AddInt32(&hits, 1) switch n { case 1: next := serverURL + "/rest/api/content/12345/child/comment?start=1" writeJSON(t, w, listResponse([]commentJSON{ footerComment("1", "

a

", "A"), }, next)) case 2: assert.Equal(t, "1", r.URL.Query().Get("start")) writeJSON(t, w, listResponse([]commentJSON{ footerComment("2", "

b

", "B"), }, "")) } })) defer server.Close() serverURL = server.URL client := NewClient(server.URL, "test-token") result, err := client.GetComments("12345") require.NoError(t, err) require.Len(t, result, 2) assert.Equal(t, int32(2), atomic.LoadInt32(&hits)) } func TestGetComments_ExpandParam(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "all", r.URL.Query().Get("depth")) assert.Equal(t, fmt.Sprintf("%d", commentLimit), r.URL.Query().Get("limit")) expand := r.URL.Query().Get("expand") assert.Equal(t, commentExpand, expand) assert.True(t, strings.Contains(expand, "extensions.inlineProperties")) assert.True(t, strings.Contains(expand, "children.comment.children.comment")) writeJSON(t, w, listResponse(nil, "")) })) defer server.Close() client := NewClient(server.URL, "test-token") _, err := client.GetComments("12345") require.NoError(t, err) } func TestGetComments_NotFound(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) })) defer server.Close() client := NewClient(server.URL, "test-token") _, err := client.GetComments("12345") require.Error(t, err) assert.Contains(t, err.Error(), "not found") } func TestGetComments_MissingOptionalFields(t *testing.T) { // Footer comment without resolution/inlineProperties; minimum viable shape. c := commentJSON{ "id": "999", "type": "comment", "status": "current", "body": map[string]any{ "storage": map[string]any{"value": "

hi

", "representation": "storage"}, }, "extensions": map[string]any{"location": "footer"}, } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { writeJSON(t, w, listResponse([]commentJSON{c}, "")) })) defer server.Close() client := NewClient(server.URL, "test-token") result, err := client.GetComments("12345") require.NoError(t, err) require.Len(t, result, 1) assert.Equal(t, "footer", result[0].Location) assert.Empty(t, result[0].MarkerRef) assert.False(t, result[0].Resolved) assert.Empty(t, result[0].Resolution) assert.Empty(t, result[0].Author.DisplayName) }