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:
Steve Yegge
2025-10-12 01:24:21 -07:00
parent 15afb5ad17
commit 9a768ba4a3
2 changed files with 627 additions and 0 deletions

View 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
}