Add comprehensive test coverage for types and export/import
## Test Coverage Improvements **Before:** - cmd/bd: 0% - internal/types: 0% - internal/storage/sqlite: 57.8% **After:** - cmd/bd: 8.5% - internal/types: 100% - internal/storage/sqlite: 57.8% ## New Tests ### types_test.go (100% coverage) - TestIssueValidation: All validation rules (title, priority, status, etc.) - TestStatusIsValid: All status values - TestIssueTypeIsValid: All issue types - TestDependencyTypeIsValid: All dependency types - TestIssueStructFields: Time field handling - TestBlockedIssueEmbedding: Embedded struct access - TestTreeNodeEmbedding: Tree node structure ### export_import_test.go (integration tests) - TestExportImport: Full export/import workflow - Export with sorting verification - JSONL format validation - Import creates new issues - Import updates existing issues - Export with status filtering - TestExportEmpty: Empty database handling - TestImportInvalidJSON: Error handling for malformed JSON - TestRoundTrip: Data integrity verification (all fields preserved) ## Test Quality - Uses table-driven tests for comprehensive coverage - Tests both happy paths and error cases - Validates JSONL format correctness - Ensures data integrity through round-trip testing - Covers edge cases (empty DB, invalid JSON, pointer fields) All tests passing ✅ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
301
internal/types/types_test.go
Normal file
301
internal/types/types_test.go
Normal file
@@ -0,0 +1,301 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIssueValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
issue Issue
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid issue",
|
||||
issue: Issue{
|
||||
ID: "test-1",
|
||||
Title: "Valid issue",
|
||||
Description: "Description",
|
||||
Status: StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: TypeFeature,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing title",
|
||||
issue: Issue{
|
||||
ID: "test-1",
|
||||
Status: StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: TypeFeature,
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "title is required",
|
||||
},
|
||||
{
|
||||
name: "title too long",
|
||||
issue: Issue{
|
||||
ID: "test-1",
|
||||
Title: string(make([]byte, 501)), // 501 characters
|
||||
Status: StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: TypeFeature,
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "title must be 500 characters or less",
|
||||
},
|
||||
{
|
||||
name: "invalid priority too low",
|
||||
issue: Issue{
|
||||
ID: "test-1",
|
||||
Title: "Test",
|
||||
Status: StatusOpen,
|
||||
Priority: -1,
|
||||
IssueType: TypeFeature,
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "priority must be between 0 and 4",
|
||||
},
|
||||
{
|
||||
name: "invalid priority too high",
|
||||
issue: Issue{
|
||||
ID: "test-1",
|
||||
Title: "Test",
|
||||
Status: StatusOpen,
|
||||
Priority: 5,
|
||||
IssueType: TypeFeature,
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "priority must be between 0 and 4",
|
||||
},
|
||||
{
|
||||
name: "invalid status",
|
||||
issue: Issue{
|
||||
ID: "test-1",
|
||||
Title: "Test",
|
||||
Status: Status("invalid"),
|
||||
Priority: 2,
|
||||
IssueType: TypeFeature,
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "invalid status",
|
||||
},
|
||||
{
|
||||
name: "invalid issue type",
|
||||
issue: Issue{
|
||||
ID: "test-1",
|
||||
Title: "Test",
|
||||
Status: StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: IssueType("invalid"),
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "invalid issue type",
|
||||
},
|
||||
{
|
||||
name: "negative estimated minutes",
|
||||
issue: Issue{
|
||||
ID: "test-1",
|
||||
Title: "Test",
|
||||
Status: StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: TypeFeature,
|
||||
EstimatedMinutes: intPtr(-10),
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "estimated_minutes cannot be negative",
|
||||
},
|
||||
{
|
||||
name: "valid estimated minutes",
|
||||
issue: Issue{
|
||||
ID: "test-1",
|
||||
Title: "Test",
|
||||
Status: StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: TypeFeature,
|
||||
EstimatedMinutes: intPtr(60),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.issue.Validate()
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("Validate() expected error containing %q, got nil", tt.errMsg)
|
||||
return
|
||||
}
|
||||
if tt.errMsg != "" && !contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("Validate() error = %v, want error containing %q", err, tt.errMsg)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Validate() unexpected error = %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusIsValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
status Status
|
||||
valid bool
|
||||
}{
|
||||
{StatusOpen, true},
|
||||
{StatusInProgress, true},
|
||||
{StatusBlocked, true},
|
||||
{StatusClosed, true},
|
||||
{Status("invalid"), false},
|
||||
{Status(""), false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.status), func(t *testing.T) {
|
||||
if got := tt.status.IsValid(); got != tt.valid {
|
||||
t.Errorf("Status(%q).IsValid() = %v, want %v", tt.status, got, tt.valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueTypeIsValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
issueType IssueType
|
||||
valid bool
|
||||
}{
|
||||
{TypeBug, true},
|
||||
{TypeFeature, true},
|
||||
{TypeTask, true},
|
||||
{TypeEpic, true},
|
||||
{TypeChore, true},
|
||||
{IssueType("invalid"), false},
|
||||
{IssueType(""), false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.issueType), func(t *testing.T) {
|
||||
if got := tt.issueType.IsValid(); got != tt.valid {
|
||||
t.Errorf("IssueType(%q).IsValid() = %v, want %v", tt.issueType, got, tt.valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDependencyTypeIsValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
depType DependencyType
|
||||
valid bool
|
||||
}{
|
||||
{DepBlocks, true},
|
||||
{DepRelated, true},
|
||||
{DepParentChild, true},
|
||||
{DepDiscoveredFrom, true},
|
||||
{DependencyType("invalid"), false},
|
||||
{DependencyType(""), false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.depType), func(t *testing.T) {
|
||||
if got := tt.depType.IsValid(); got != tt.valid {
|
||||
t.Errorf("DependencyType(%q).IsValid() = %v, want %v", tt.depType, got, tt.valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueStructFields(t *testing.T) {
|
||||
// Test that all time fields work correctly
|
||||
now := time.Now()
|
||||
closedAt := now.Add(time.Hour)
|
||||
|
||||
issue := Issue{
|
||||
ID: "test-1",
|
||||
Title: "Test Issue",
|
||||
Description: "Test description",
|
||||
Status: StatusClosed,
|
||||
Priority: 1,
|
||||
IssueType: TypeBug,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
ClosedAt: &closedAt,
|
||||
}
|
||||
|
||||
if issue.CreatedAt != now {
|
||||
t.Errorf("CreatedAt = %v, want %v", issue.CreatedAt, now)
|
||||
}
|
||||
if issue.ClosedAt == nil || *issue.ClosedAt != closedAt {
|
||||
t.Errorf("ClosedAt = %v, want %v", issue.ClosedAt, closedAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockedIssueEmbedding(t *testing.T) {
|
||||
blocked := BlockedIssue{
|
||||
Issue: Issue{
|
||||
ID: "test-1",
|
||||
Title: "Blocked issue",
|
||||
Status: StatusBlocked,
|
||||
Priority: 2,
|
||||
IssueType: TypeFeature,
|
||||
},
|
||||
BlockedByCount: 2,
|
||||
BlockedBy: []string{"test-2", "test-3"},
|
||||
}
|
||||
|
||||
// Test that embedded Issue fields are accessible
|
||||
if blocked.ID != "test-1" {
|
||||
t.Errorf("BlockedIssue.ID = %q, want %q", blocked.ID, "test-1")
|
||||
}
|
||||
if blocked.BlockedByCount != 2 {
|
||||
t.Errorf("BlockedByCount = %d, want 2", blocked.BlockedByCount)
|
||||
}
|
||||
if len(blocked.BlockedBy) != 2 {
|
||||
t.Errorf("len(BlockedBy) = %d, want 2", len(blocked.BlockedBy))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeNodeEmbedding(t *testing.T) {
|
||||
node := TreeNode{
|
||||
Issue: Issue{
|
||||
ID: "test-1",
|
||||
Title: "Root node",
|
||||
Status: StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: TypeEpic,
|
||||
},
|
||||
Depth: 0,
|
||||
Truncated: false,
|
||||
}
|
||||
|
||||
// Test that embedded Issue fields are accessible
|
||||
if node.ID != "test-1" {
|
||||
t.Errorf("TreeNode.ID = %q, want %q", node.ID, "test-1")
|
||||
}
|
||||
if node.Depth != 0 {
|
||||
t.Errorf("Depth = %d, want 0", node.Depth)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func intPtr(i int) *int {
|
||||
return &i
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || containsMiddle(s, substr)))
|
||||
}
|
||||
|
||||
func containsMiddle(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user