test: add comprehensive Comments API tests (bd-d68f)
- Test AddIssueComment basic functionality - Test GetIssueComments retrieval and ordering - Test edge cases (empty, nonexistent issues) - Test dirty flag marking - Test comment isolation across issues Improves sqlite package coverage: 69.1% → 70.6%
This commit is contained in:
368
internal/storage/sqlite/comments_test.go
Normal file
368
internal/storage/sqlite/comments_test.go
Normal file
@@ -0,0 +1,368 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
// TestAddIssueComment tests basic comment addition
|
||||
func TestAddIssueComment(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create an issue
|
||||
issue := &types.Issue{
|
||||
Title: "Test issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, issue, "test-user"); err != nil {
|
||||
t.Fatalf("CreateIssue failed: %v", err)
|
||||
}
|
||||
|
||||
// Add a comment
|
||||
comment, err := store.AddIssueComment(ctx, issue.ID, "alice", "This is a test comment")
|
||||
if err != nil {
|
||||
t.Fatalf("AddIssueComment failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify comment fields
|
||||
if comment.IssueID != issue.ID {
|
||||
t.Errorf("Expected IssueID %s, got %s", issue.ID, comment.IssueID)
|
||||
}
|
||||
if comment.Author != "alice" {
|
||||
t.Errorf("Expected Author 'alice', got '%s'", comment.Author)
|
||||
}
|
||||
if comment.Text != "This is a test comment" {
|
||||
t.Errorf("Expected Text 'This is a test comment', got '%s'", comment.Text)
|
||||
}
|
||||
if comment.ID == 0 {
|
||||
t.Error("Expected non-zero comment ID")
|
||||
}
|
||||
if comment.CreatedAt.IsZero() {
|
||||
t.Error("Expected non-zero CreatedAt timestamp")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddIssueCommentNonexistentIssue tests adding comment to non-existent issue
|
||||
func TestAddIssueCommentNonexistentIssue(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Try to add comment to non-existent issue
|
||||
_, err := store.AddIssueComment(ctx, "nonexistent-id", "alice", "comment")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error when adding comment to non-existent issue, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetIssueComments tests retrieving comments
|
||||
func TestGetIssueComments(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create an issue
|
||||
issue := &types.Issue{
|
||||
Title: "Test issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, issue, "test-user"); err != nil {
|
||||
t.Fatalf("CreateIssue failed: %v", err)
|
||||
}
|
||||
|
||||
// Add multiple comments
|
||||
testComments := []struct {
|
||||
author string
|
||||
text string
|
||||
}{
|
||||
{"alice", "First comment"},
|
||||
{"bob", "Second comment"},
|
||||
{"charlie", "Third comment"},
|
||||
}
|
||||
|
||||
for _, tc := range testComments {
|
||||
_, err := store.AddIssueComment(ctx, issue.ID, tc.author, tc.text)
|
||||
if err != nil {
|
||||
t.Fatalf("AddIssueComment failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve comments
|
||||
comments, err := store.GetIssueComments(ctx, issue.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssueComments failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify number of comments
|
||||
if len(comments) != len(testComments) {
|
||||
t.Fatalf("Expected %d comments, got %d", len(testComments), len(comments))
|
||||
}
|
||||
|
||||
// Verify comment content and ordering (should be chronological)
|
||||
for i, comment := range comments {
|
||||
if comment.Author != testComments[i].author {
|
||||
t.Errorf("Comment %d: expected author %s, got %s", i, testComments[i].author, comment.Author)
|
||||
}
|
||||
if comment.Text != testComments[i].text {
|
||||
t.Errorf("Comment %d: expected text %s, got %s", i, testComments[i].text, comment.Text)
|
||||
}
|
||||
if comment.IssueID != issue.ID {
|
||||
t.Errorf("Comment %d: expected IssueID %s, got %s", i, issue.ID, comment.IssueID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetIssueCommentsOrdering tests that comments are returned in chronological order
|
||||
func TestGetIssueCommentsOrdering(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create an issue
|
||||
issue := &types.Issue{
|
||||
Title: "Test issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, issue, "test-user"); err != nil {
|
||||
t.Fatalf("CreateIssue failed: %v", err)
|
||||
}
|
||||
|
||||
// Add comments with identifiable ordering
|
||||
for i := 1; i <= 5; i++ {
|
||||
text := "Comment " + string(rune('0'+i))
|
||||
_, err := store.AddIssueComment(ctx, issue.ID, "alice", text)
|
||||
if err != nil {
|
||||
t.Fatalf("AddIssueComment failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve comments
|
||||
comments, err := store.GetIssueComments(ctx, issue.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssueComments failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify chronological ordering
|
||||
if len(comments) != 5 {
|
||||
t.Fatalf("Expected 5 comments, got %d", len(comments))
|
||||
}
|
||||
|
||||
for i := 0; i < len(comments); i++ {
|
||||
expectedText := "Comment " + string(rune('0'+i+1))
|
||||
if comments[i].Text != expectedText {
|
||||
t.Errorf("Comment %d: expected text %s, got %s", i, expectedText, comments[i].Text)
|
||||
}
|
||||
|
||||
// Verify timestamps are in ascending order
|
||||
if i > 0 && comments[i].CreatedAt.Before(comments[i-1].CreatedAt) {
|
||||
t.Errorf("Comments not in chronological order: comment %d (%v) is before comment %d (%v)",
|
||||
i, comments[i].CreatedAt, i-1, comments[i-1].CreatedAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetIssueCommentsEmpty tests retrieving comments for issue with no comments
|
||||
func TestGetIssueCommentsEmpty(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create an issue
|
||||
issue := &types.Issue{
|
||||
Title: "Test issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, issue, "test-user"); err != nil {
|
||||
t.Fatalf("CreateIssue failed: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve comments (should be empty)
|
||||
comments, err := store.GetIssueComments(ctx, issue.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssueComments failed: %v", err)
|
||||
}
|
||||
|
||||
if len(comments) != 0 {
|
||||
t.Errorf("Expected 0 comments, got %d", len(comments))
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetIssueCommentsNonexistentIssue tests retrieving comments for non-existent issue
|
||||
func TestGetIssueCommentsNonexistentIssue(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Retrieve comments for non-existent issue
|
||||
comments, err := store.GetIssueComments(ctx, "nonexistent-id")
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssueComments failed: %v", err)
|
||||
}
|
||||
|
||||
// Should return empty slice, not error
|
||||
if len(comments) != 0 {
|
||||
t.Errorf("Expected 0 comments for non-existent issue, got %d", len(comments))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddIssueCommentEmptyText tests adding comment with empty text
|
||||
func TestAddIssueCommentEmptyText(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create an issue
|
||||
issue := &types.Issue{
|
||||
Title: "Test issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, issue, "test-user"); err != nil {
|
||||
t.Fatalf("CreateIssue failed: %v", err)
|
||||
}
|
||||
|
||||
// Add comment with empty text (should succeed - validation is caller's responsibility)
|
||||
comment, err := store.AddIssueComment(ctx, issue.ID, "alice", "")
|
||||
if err != nil {
|
||||
t.Fatalf("AddIssueComment with empty text failed: %v", err)
|
||||
}
|
||||
|
||||
if comment.Text != "" {
|
||||
t.Errorf("Expected empty text, got '%s'", comment.Text)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddIssueCommentMarksDirty tests that adding a comment marks the issue dirty
|
||||
func TestAddIssueCommentMarksDirty(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create an issue
|
||||
issue := &types.Issue{
|
||||
Title: "Test issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, issue, "test-user"); err != nil {
|
||||
t.Fatalf("CreateIssue failed: %v", err)
|
||||
}
|
||||
|
||||
// Clear dirty flag (simulating after export)
|
||||
if err := store.ClearDirtyIssuesByID(ctx, []string{issue.ID}); err != nil {
|
||||
t.Fatalf("ClearDirtyIssuesByID failed: %v", err)
|
||||
}
|
||||
|
||||
// Add a comment
|
||||
_, err := store.AddIssueComment(ctx, issue.ID, "alice", "test comment")
|
||||
if err != nil {
|
||||
t.Fatalf("AddIssueComment failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify issue is marked dirty
|
||||
var exists bool
|
||||
err = store.db.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM dirty_issues WHERE issue_id = ?)`, issue.ID).Scan(&exists)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check dirty flag: %v", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
t.Error("Expected issue to be marked dirty after adding comment")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetIssueCommentsMultipleIssues tests that comments are properly isolated per issue
|
||||
func TestGetIssueCommentsMultipleIssues(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create two issues
|
||||
issue1 := &types.Issue{
|
||||
Title: "Issue 1",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
issue2 := &types.Issue{
|
||||
Title: "Issue 2",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, issue1, "test-user"); err != nil {
|
||||
t.Fatalf("CreateIssue failed: %v", err)
|
||||
}
|
||||
if err := store.CreateIssue(ctx, issue2, "test-user"); err != nil {
|
||||
t.Fatalf("CreateIssue failed: %v", err)
|
||||
}
|
||||
|
||||
// Add comments to each issue
|
||||
_, err := store.AddIssueComment(ctx, issue1.ID, "alice", "Comment for issue 1")
|
||||
if err != nil {
|
||||
t.Fatalf("AddIssueComment failed: %v", err)
|
||||
}
|
||||
_, err = store.AddIssueComment(ctx, issue1.ID, "bob", "Another comment for issue 1")
|
||||
if err != nil {
|
||||
t.Fatalf("AddIssueComment failed: %v", err)
|
||||
}
|
||||
_, err = store.AddIssueComment(ctx, issue2.ID, "charlie", "Comment for issue 2")
|
||||
if err != nil {
|
||||
t.Fatalf("AddIssueComment failed: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve comments for issue 1
|
||||
comments1, err := store.GetIssueComments(ctx, issue1.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssueComments failed: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve comments for issue 2
|
||||
comments2, err := store.GetIssueComments(ctx, issue2.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssueComments failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify each issue has the correct number of comments
|
||||
if len(comments1) != 2 {
|
||||
t.Errorf("Expected 2 comments for issue 1, got %d", len(comments1))
|
||||
}
|
||||
if len(comments2) != 1 {
|
||||
t.Errorf("Expected 1 comment for issue 2, got %d", len(comments2))
|
||||
}
|
||||
|
||||
// Verify comments belong to correct issues
|
||||
for _, c := range comments1 {
|
||||
if c.IssueID != issue1.ID {
|
||||
t.Errorf("Comment has wrong IssueID: expected %s, got %s", issue1.ID, c.IssueID)
|
||||
}
|
||||
}
|
||||
for _, c := range comments2 {
|
||||
if c.IssueID != issue2.ID {
|
||||
t.Errorf("Comment has wrong IssueID: expected %s, got %s", issue2.ID, c.IssueID)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user