* feat: add `docs comments` subcommand for managing Google Doc comments Add a full `gog docs comments` command tree that provides a doc-centric interface for managing comments on Google Docs via the Drive API v3 comments endpoint. Subcommands: list - List comments (filters resolved by default, --include-resolved to show all) get - Get a single comment with full details including replies add - Add a comment, optionally anchored to quoted text reply - Reply to a comment resolve - Resolve a comment (mark as done), with optional message delete - Delete a comment Key differences from `drive comments`: - `list` filters resolved comments by default (--include-resolved to include) - `list` always shows quoted text column (no separate flag needed) - `resolve` subcommand (drive comments has no equivalent) - Accepts Google Doc URLs in addition to bare IDs (via normalizeGoogleID) - Doc-centric naming and help text Follows existing patterns: uses shared helpers (collectAllPages, tableWriter, formatDateTime, truncateString, normalizeGoogleID, failEmptyExit, dryRunExit, confirmDestructive), same auth/token flow, consistent output formatting. * fix: improve docs comments list and anchoring (#263) (thanks @alextnetto) --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
592 lines
17 KiB
Go
592 lines
17 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"google.golang.org/api/drive/v3"
|
|
"google.golang.org/api/option"
|
|
|
|
"github.com/steipete/gogcli/internal/ui"
|
|
)
|
|
|
|
// newCommentsTestServer returns a test server that handles the Drive comments API
|
|
// endpoints needed by docs comments commands.
|
|
func newCommentsTestServer(t *testing.T) *httptest.Server {
|
|
t.Helper()
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/drive/v3")
|
|
switch {
|
|
// List comments
|
|
case r.Method == http.MethodGet && path == "/files/doc1/comments":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"comments": []map[string]any{
|
|
{
|
|
"id": "c1",
|
|
"author": map[string]any{"displayName": "Alice"},
|
|
"content": "Needs revision",
|
|
"createdTime": "2025-06-01T10:00:00Z",
|
|
"resolved": false,
|
|
"quotedFileContent": map[string]any{
|
|
"value": "The quick brown fox",
|
|
},
|
|
"replies": []map[string]any{
|
|
{
|
|
"id": "r1",
|
|
"author": map[string]any{"displayName": "Bob"},
|
|
"content": "Working on it",
|
|
"createdTime": "2025-06-01T11:00:00Z",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"id": "c2",
|
|
"author": map[string]any{"displayName": "Charlie"},
|
|
"content": "LGTM",
|
|
"createdTime": "2025-06-01T09:00:00Z",
|
|
"resolved": true,
|
|
},
|
|
},
|
|
})
|
|
return
|
|
|
|
// List comments: first page has only resolved, second page has open.
|
|
case r.Method == http.MethodGet && path == "/files/scan/comments":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if r.URL.Query().Get("pageToken") == "p2" {
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"comments": []map[string]any{
|
|
{
|
|
"id": "c-open",
|
|
"author": map[string]any{"displayName": "Dana"},
|
|
"content": "Open comment",
|
|
"createdTime": "2025-06-03T10:00:00Z",
|
|
"resolved": false,
|
|
},
|
|
},
|
|
})
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"comments": []map[string]any{
|
|
{
|
|
"id": "c-res",
|
|
"author": map[string]any{"displayName": "Eli"},
|
|
"content": "Resolved only",
|
|
"createdTime": "2025-06-03T09:00:00Z",
|
|
"resolved": true,
|
|
},
|
|
},
|
|
"nextPageToken": "p2",
|
|
})
|
|
return
|
|
|
|
// List comments on empty doc
|
|
case r.Method == http.MethodGet && path == "/files/empty/comments":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"comments": []any{}})
|
|
return
|
|
|
|
// Get single comment
|
|
case r.Method == http.MethodGet && path == "/files/doc1/comments/c1":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"id": "c1",
|
|
"author": map[string]any{"displayName": "Alice"},
|
|
"content": "Needs revision",
|
|
"createdTime": "2025-06-01T10:00:00Z",
|
|
"modifiedTime": "2025-06-01T10:30:00Z",
|
|
"resolved": false,
|
|
"quotedFileContent": map[string]any{
|
|
"value": "The quick brown fox",
|
|
},
|
|
"replies": []map[string]any{
|
|
{
|
|
"id": "r1",
|
|
"author": map[string]any{"displayName": "Bob"},
|
|
"content": "Working on it",
|
|
"createdTime": "2025-06-01T11:00:00Z",
|
|
},
|
|
},
|
|
})
|
|
return
|
|
|
|
// Create comment
|
|
case r.Method == http.MethodPost && path == "/files/doc1/comments":
|
|
var body struct {
|
|
Content string `json:"content"`
|
|
Anchor string `json:"anchor"`
|
|
QuotedFileContent struct {
|
|
Value string `json:"value"`
|
|
} `json:"quotedFileContent"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"id": "c3",
|
|
"content": body.Content,
|
|
"createdTime": "2025-06-02T08:00:00Z",
|
|
"anchor": body.Anchor,
|
|
"quotedFileContent": map[string]any{
|
|
"value": body.QuotedFileContent.Value,
|
|
},
|
|
})
|
|
return
|
|
|
|
// Delete comment
|
|
case r.Method == http.MethodDelete && path == "/files/doc1/comments/c1":
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
|
|
// Create reply
|
|
case r.Method == http.MethodPost && path == "/files/doc1/comments/c1/replies":
|
|
var body struct {
|
|
Content string `json:"content"`
|
|
Action string `json:"action"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
resp := map[string]any{
|
|
"id": "r2",
|
|
"content": body.Content,
|
|
"createdTime": "2025-06-02T09:00:00Z",
|
|
}
|
|
if body.Action != "" {
|
|
resp["action"] = body.Action
|
|
}
|
|
_ = json.NewEncoder(w).Encode(resp)
|
|
return
|
|
|
|
default:
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
}))
|
|
}
|
|
|
|
func setupDriveServiceFromServer(t *testing.T, srv *httptest.Server) {
|
|
t.Helper()
|
|
origNew := newDriveService
|
|
t.Cleanup(func() { newDriveService = origNew })
|
|
|
|
svc, err := drive.NewService(context.Background(),
|
|
option.WithoutAuthentication(),
|
|
option.WithHTTPClient(srv.Client()),
|
|
option.WithEndpoint(srv.URL+"/"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewService: %v", err)
|
|
}
|
|
newDriveService = func(context.Context, string) (*drive.Service, error) { return svc, nil }
|
|
}
|
|
|
|
func TestDocsCommentsList_FiltersResolved(t *testing.T) {
|
|
srv := newCommentsTestServer(t)
|
|
defer srv.Close()
|
|
setupDriveServiceFromServer(t, srv)
|
|
|
|
// Default: open only
|
|
jsonOut := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--json", "--account", "a@b.com", "docs", "comments", "list", "doc1"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
var parsed struct {
|
|
DocID string `json:"docId"`
|
|
Comments []*drive.Comment `json:"comments"`
|
|
}
|
|
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
|
|
t.Fatalf("json parse: %v\nout=%q", err, jsonOut)
|
|
}
|
|
if parsed.DocID != "doc1" {
|
|
t.Fatalf("expected docId=doc1, got %q", parsed.DocID)
|
|
}
|
|
// Should only have the open comment (c1), not the resolved one (c2)
|
|
if len(parsed.Comments) != 1 {
|
|
t.Fatalf("expected 1 open comment, got %d", len(parsed.Comments))
|
|
}
|
|
if parsed.Comments[0].Id != "c1" {
|
|
t.Fatalf("expected comment c1, got %q", parsed.Comments[0].Id)
|
|
}
|
|
}
|
|
|
|
func TestDocsCommentsList_IncludeResolved(t *testing.T) {
|
|
srv := newCommentsTestServer(t)
|
|
defer srv.Close()
|
|
setupDriveServiceFromServer(t, srv)
|
|
|
|
jsonOut := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--json", "--account", "a@b.com", "docs", "comments", "list", "--include-resolved", "doc1"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
var parsed struct {
|
|
Comments []*drive.Comment `json:"comments"`
|
|
}
|
|
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
|
|
t.Fatalf("json parse: %v\nout=%q", err, jsonOut)
|
|
}
|
|
if len(parsed.Comments) != 2 {
|
|
t.Fatalf("expected 2 comments with --include-resolved, got %d", len(parsed.Comments))
|
|
}
|
|
}
|
|
|
|
func TestDocsCommentsList_PlainText(t *testing.T) {
|
|
srv := newCommentsTestServer(t)
|
|
defer srv.Close()
|
|
setupDriveServiceFromServer(t, srv)
|
|
|
|
out := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--account", "a@b.com", "docs", "comments", "list", "doc1"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
if !strings.Contains(out, "Alice") {
|
|
t.Fatalf("expected author Alice in output, got: %q", out)
|
|
}
|
|
if !strings.Contains(out, "Needs revision") {
|
|
t.Fatalf("expected comment content in output, got: %q", out)
|
|
}
|
|
if !strings.Contains(out, "TYPE") {
|
|
t.Fatalf("expected TYPE header in output, got: %q", out)
|
|
}
|
|
if !strings.Contains(out, "Working on it") {
|
|
t.Fatalf("expected reply content in output, got: %q", out)
|
|
}
|
|
// Resolved comment should be filtered out in default mode
|
|
if strings.Contains(out, "LGTM") {
|
|
t.Fatalf("resolved comment should be filtered, got: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestDocsCommentsList_ScansPagesForOpenComments(t *testing.T) {
|
|
srv := newCommentsTestServer(t)
|
|
defer srv.Close()
|
|
setupDriveServiceFromServer(t, srv)
|
|
|
|
jsonOut := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--json", "--account", "a@b.com", "docs", "comments", "list", "scan"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
var parsed struct {
|
|
Comments []*drive.Comment `json:"comments"`
|
|
}
|
|
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
|
|
t.Fatalf("json parse: %v\nout=%q", err, jsonOut)
|
|
}
|
|
if len(parsed.Comments) != 1 || parsed.Comments[0].Id != "c-open" {
|
|
t.Fatalf("expected scan to return open comment, got %#v", parsed.Comments)
|
|
}
|
|
}
|
|
|
|
func TestDocsCommentsList_Empty(t *testing.T) {
|
|
srv := newCommentsTestServer(t)
|
|
defer srv.Close()
|
|
setupDriveServiceFromServer(t, srv)
|
|
|
|
errOut := captureStderr(t, func() {
|
|
if err := Execute([]string{"--account", "a@b.com", "docs", "comments", "list", "empty"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
if !strings.Contains(errOut, "No comments") {
|
|
t.Fatalf("expected 'No comments' in stderr, got: %q", errOut)
|
|
}
|
|
}
|
|
|
|
func TestDocsCommentsGet_JSON(t *testing.T) {
|
|
srv := newCommentsTestServer(t)
|
|
defer srv.Close()
|
|
setupDriveServiceFromServer(t, srv)
|
|
|
|
jsonOut := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--json", "--account", "a@b.com", "docs", "comments", "get", "doc1", "c1"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
var parsed struct {
|
|
Comment *drive.Comment `json:"comment"`
|
|
}
|
|
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
|
|
t.Fatalf("json parse: %v\nout=%q", err, jsonOut)
|
|
}
|
|
if parsed.Comment == nil || parsed.Comment.Id != "c1" {
|
|
t.Fatalf("unexpected comment: %#v", parsed.Comment)
|
|
}
|
|
if parsed.Comment.QuotedFileContent == nil || parsed.Comment.QuotedFileContent.Value != "The quick brown fox" {
|
|
t.Fatalf("missing quoted content: %#v", parsed.Comment)
|
|
}
|
|
}
|
|
|
|
func TestDocsCommentsGet_Plain(t *testing.T) {
|
|
srv := newCommentsTestServer(t)
|
|
defer srv.Close()
|
|
setupDriveServiceFromServer(t, srv)
|
|
|
|
out := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--account", "a@b.com", "docs", "comments", "get", "doc1", "c1"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
if !strings.Contains(out, "Alice") {
|
|
t.Fatalf("expected author in output: %q", out)
|
|
}
|
|
if !strings.Contains(out, "Needs revision") {
|
|
t.Fatalf("expected content in output: %q", out)
|
|
}
|
|
if !strings.Contains(out, "The quick brown fox") {
|
|
t.Fatalf("expected quoted text in output: %q", out)
|
|
}
|
|
if !strings.Contains(out, "reply") {
|
|
t.Fatalf("expected reply info in output: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestDocsCommentsAdd_JSON(t *testing.T) {
|
|
srv := newCommentsTestServer(t)
|
|
defer srv.Close()
|
|
setupDriveServiceFromServer(t, srv)
|
|
|
|
jsonOut := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--json", "--account", "a@b.com", "docs", "comments", "add", "doc1", "Nice work", "--quoted", "some text", "--anchor", "{\"a\":1}"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
var parsed struct {
|
|
Comment *drive.Comment `json:"comment"`
|
|
}
|
|
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
|
|
t.Fatalf("json parse: %v\nout=%q", err, jsonOut)
|
|
}
|
|
if parsed.Comment == nil || parsed.Comment.Id != "c3" {
|
|
t.Fatalf("unexpected comment: %#v", parsed.Comment)
|
|
}
|
|
if parsed.Comment.Content != "Nice work" {
|
|
t.Fatalf("expected content 'Nice work', got %q", parsed.Comment.Content)
|
|
}
|
|
if parsed.Comment.Anchor != "{\"a\":1}" {
|
|
t.Fatalf("expected anchor, got %q", parsed.Comment.Anchor)
|
|
}
|
|
}
|
|
|
|
func TestDocsCommentsAdd_Plain(t *testing.T) {
|
|
srv := newCommentsTestServer(t)
|
|
defer srv.Close()
|
|
setupDriveServiceFromServer(t, srv)
|
|
|
|
out := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--account", "a@b.com", "docs", "comments", "add", "doc1", "A comment"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
if !strings.Contains(out, "c3") {
|
|
t.Fatalf("expected comment ID in output: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestDocsCommentsReply_JSON(t *testing.T) {
|
|
srv := newCommentsTestServer(t)
|
|
defer srv.Close()
|
|
setupDriveServiceFromServer(t, srv)
|
|
|
|
jsonOut := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--json", "--account", "a@b.com", "docs", "comments", "reply", "doc1", "c1", "Thanks!"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
var parsed struct {
|
|
Reply *drive.Reply `json:"reply"`
|
|
}
|
|
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
|
|
t.Fatalf("json parse: %v\nout=%q", err, jsonOut)
|
|
}
|
|
if parsed.Reply == nil || parsed.Reply.Id != "r2" {
|
|
t.Fatalf("unexpected reply: %#v", parsed.Reply)
|
|
}
|
|
}
|
|
|
|
func TestDocsCommentsResolve_JSON(t *testing.T) {
|
|
srv := newCommentsTestServer(t)
|
|
defer srv.Close()
|
|
setupDriveServiceFromServer(t, srv)
|
|
|
|
jsonOut := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--json", "--account", "a@b.com", "docs", "comments", "resolve", "doc1", "c1"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
var parsed struct {
|
|
Resolved bool `json:"resolved"`
|
|
DocID string `json:"docId"`
|
|
CommentID string `json:"commentId"`
|
|
}
|
|
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
|
|
t.Fatalf("json parse: %v\nout=%q", err, jsonOut)
|
|
}
|
|
if !parsed.Resolved || parsed.DocID != "doc1" || parsed.CommentID != "c1" {
|
|
t.Fatalf("unexpected resolve output: %#v", parsed)
|
|
}
|
|
}
|
|
|
|
func TestDocsCommentsDelete_JSON(t *testing.T) {
|
|
srv := newCommentsTestServer(t)
|
|
defer srv.Close()
|
|
setupDriveServiceFromServer(t, srv)
|
|
|
|
jsonOut := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--json", "--force", "--account", "a@b.com", "docs", "comments", "delete", "doc1", "c1"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
var parsed struct {
|
|
Deleted bool `json:"deleted"`
|
|
DocID string `json:"docId"`
|
|
CommentID string `json:"commentId"`
|
|
}
|
|
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
|
|
t.Fatalf("json parse: %v\nout=%q", err, jsonOut)
|
|
}
|
|
if !parsed.Deleted || parsed.DocID != "doc1" || parsed.CommentID != "c1" {
|
|
t.Fatalf("unexpected delete output: %#v", parsed)
|
|
}
|
|
}
|
|
|
|
func TestDocsComments_ValidationErrors(t *testing.T) {
|
|
u, uiErr := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
|
|
if uiErr != nil {
|
|
t.Fatalf("ui.New: %v", uiErr)
|
|
}
|
|
ctx := ui.WithUI(context.Background(), u)
|
|
flags := &RootFlags{Account: "a@b.com"}
|
|
|
|
if err := (&DocsCommentsListCmd{}).Run(ctx, flags); err == nil {
|
|
t.Fatal("expected list missing docId error")
|
|
}
|
|
if err := (&DocsCommentsGetCmd{}).Run(ctx, flags); err == nil {
|
|
t.Fatal("expected get missing docId error")
|
|
}
|
|
if err := (&DocsCommentsGetCmd{DocID: "d1"}).Run(ctx, flags); err == nil {
|
|
t.Fatal("expected get missing commentId error")
|
|
}
|
|
if err := (&DocsCommentsAddCmd{}).Run(ctx, flags); err == nil {
|
|
t.Fatal("expected add missing docId error")
|
|
}
|
|
if err := (&DocsCommentsAddCmd{DocID: "d1"}).Run(ctx, flags); err == nil {
|
|
t.Fatal("expected add missing content error")
|
|
}
|
|
if err := (&DocsCommentsReplyCmd{}).Run(ctx, flags); err == nil {
|
|
t.Fatal("expected reply missing docId error")
|
|
}
|
|
if err := (&DocsCommentsReplyCmd{DocID: "d1"}).Run(ctx, flags); err == nil {
|
|
t.Fatal("expected reply missing commentId error")
|
|
}
|
|
if err := (&DocsCommentsReplyCmd{DocID: "d1", CommentID: "c1"}).Run(ctx, flags); err == nil {
|
|
t.Fatal("expected reply missing content error")
|
|
}
|
|
if err := (&DocsCommentsResolveCmd{}).Run(ctx, flags); err == nil {
|
|
t.Fatal("expected resolve missing docId error")
|
|
}
|
|
if err := (&DocsCommentsResolveCmd{DocID: "d1"}).Run(ctx, flags); err == nil {
|
|
t.Fatal("expected resolve missing commentId error")
|
|
}
|
|
if err := (&DocsCommentsDeleteCmd{}).Run(ctx, flags); err == nil {
|
|
t.Fatal("expected delete missing docId error")
|
|
}
|
|
if err := (&DocsCommentsDeleteCmd{DocID: "d1"}).Run(ctx, flags); err == nil {
|
|
t.Fatal("expected delete missing commentId error")
|
|
}
|
|
}
|
|
|
|
func TestFilterOpenComments(t *testing.T) {
|
|
comments := []*drive.Comment{
|
|
{Id: "c1", Resolved: false},
|
|
{Id: "c2", Resolved: true},
|
|
{Id: "c3", Resolved: false},
|
|
{Id: "c4", Resolved: true},
|
|
}
|
|
open := filterOpenComments(comments)
|
|
if len(open) != 2 {
|
|
t.Fatalf("expected 2 open comments, got %d", len(open))
|
|
}
|
|
if open[0].Id != "c1" || open[1].Id != "c3" {
|
|
t.Fatalf("unexpected open comments: %v, %v", open[0].Id, open[1].Id)
|
|
}
|
|
}
|
|
|
|
func TestFilterOpenComments_AllOpen(t *testing.T) {
|
|
comments := []*drive.Comment{
|
|
{Id: "c1", Resolved: false},
|
|
}
|
|
open := filterOpenComments(comments)
|
|
if len(open) != 1 {
|
|
t.Fatalf("expected 1, got %d", len(open))
|
|
}
|
|
}
|
|
|
|
func TestFilterOpenComments_AllResolved(t *testing.T) {
|
|
comments := []*drive.Comment{
|
|
{Id: "c1", Resolved: true},
|
|
}
|
|
open := filterOpenComments(comments)
|
|
if len(open) != 0 {
|
|
t.Fatalf("expected 0, got %d", len(open))
|
|
}
|
|
}
|
|
|
|
func TestFilterOpenComments_Nil(t *testing.T) {
|
|
open := filterOpenComments(nil)
|
|
if open != nil {
|
|
t.Fatalf("expected nil, got %v", open)
|
|
}
|
|
}
|
|
|
|
func TestFilterOpenComments_NilElements(t *testing.T) {
|
|
comments := []*drive.Comment{
|
|
nil,
|
|
{Id: "c1", Resolved: true},
|
|
nil,
|
|
{Id: "c2", Resolved: false},
|
|
}
|
|
open := filterOpenComments(comments)
|
|
if len(open) != 1 || open[0].Id != "c2" {
|
|
t.Fatalf("unexpected open comments: %#v", open)
|
|
}
|
|
}
|