package inspector import ( "bytes" "encoding/json" "strings" "testing" "time" ) func createTestReport() *InspectorReport { return &InspectorReport{ Summary: ReportSummary{ TotalRules: 10, RulesChecked: 8, RulesSkipped: 2, ErrorCount: 3, WarningCount: 5, PassedCount: 12, }, Violations: []ValidationResult{ { RuleName: "primary_key_naming", Level: "error", Message: "Primary key should start with 'id_'", Location: "public.users.user_id", Passed: false, Context: map[string]interface{}{ "schema": "public", "table": "users", "column": "user_id", "pattern": "^id_", }, }, { RuleName: "table_name_length", Level: "warning", Message: "Table name too long", Location: "public.very_long_table_name_that_exceeds_limits", Passed: false, Context: map[string]interface{}{ "schema": "public", "table": "very_long_table_name_that_exceeds_limits", "length": 44, "max_length": 32, }, }, }, GeneratedAt: time.Now(), Database: "testdb", SourceFormat: "postgresql", } } func TestNewMarkdownFormatter(t *testing.T) { var buf bytes.Buffer formatter := NewMarkdownFormatter(&buf) if formatter == nil { t.Fatal("NewMarkdownFormatter() returned nil") } // Buffer is not a terminal, so colors should be disabled if formatter.UseColors { t.Error("NewMarkdownFormatter() UseColors should be false for non-terminal") } } func TestNewJSONFormatter(t *testing.T) { formatter := NewJSONFormatter() if formatter == nil { t.Fatal("NewJSONFormatter() returned nil") } } func TestMarkdownFormatter_Format(t *testing.T) { report := createTestReport() var buf bytes.Buffer formatter := NewMarkdownFormatter(&buf) output, err := formatter.Format(report) if err != nil { t.Fatalf("MarkdownFormatter.Format() returned error: %v", err) } // Check that output contains expected sections if !strings.Contains(output, "# RelSpec Inspector Report") { t.Error("Markdown output missing header") } if !strings.Contains(output, "Database:") { t.Error("Markdown output missing database field") } if !strings.Contains(output, "testdb") { t.Error("Markdown output missing database name") } if !strings.Contains(output, "Summary") { t.Error("Markdown output missing summary section") } if !strings.Contains(output, "Rules Checked: 8") { t.Error("Markdown output missing rules checked count") } if !strings.Contains(output, "Errors: 3") { t.Error("Markdown output missing error count") } if !strings.Contains(output, "Warnings: 5") { t.Error("Markdown output missing warning count") } if !strings.Contains(output, "Violations") { t.Error("Markdown output missing violations section") } if !strings.Contains(output, "primary_key_naming") { t.Error("Markdown output missing rule name") } if !strings.Contains(output, "public.users.user_id") { t.Error("Markdown output missing location") } } func TestMarkdownFormatter_FormatNoViolations(t *testing.T) { report := &InspectorReport{ Summary: ReportSummary{ TotalRules: 10, RulesChecked: 10, RulesSkipped: 0, ErrorCount: 0, WarningCount: 0, PassedCount: 50, }, Violations: []ValidationResult{}, GeneratedAt: time.Now(), Database: "testdb", SourceFormat: "postgresql", } var buf bytes.Buffer formatter := NewMarkdownFormatter(&buf) output, err := formatter.Format(report) if err != nil { t.Fatalf("MarkdownFormatter.Format() returned error: %v", err) } if !strings.Contains(output, "No violations found") { t.Error("Markdown output should indicate no violations") } } func TestJSONFormatter_Format(t *testing.T) { report := createTestReport() formatter := NewJSONFormatter() output, err := formatter.Format(report) if err != nil { t.Fatalf("JSONFormatter.Format() returned error: %v", err) } // Verify it's valid JSON var decoded InspectorReport if err := json.Unmarshal([]byte(output), &decoded); err != nil { t.Fatalf("JSONFormatter.Format() produced invalid JSON: %v", err) } // Check key fields if decoded.Database != "testdb" { t.Errorf("JSON decoded Database = %q, want \"testdb\"", decoded.Database) } if decoded.Summary.ErrorCount != 3 { t.Errorf("JSON decoded ErrorCount = %d, want 3", decoded.Summary.ErrorCount) } if len(decoded.Violations) != 2 { t.Errorf("JSON decoded Violations length = %d, want 2", len(decoded.Violations)) } } func TestMarkdownFormatter_FormatHeader(t *testing.T) { var buf bytes.Buffer formatter := NewMarkdownFormatter(&buf) header := formatter.formatHeader("Test Header") if !strings.Contains(header, "# Test Header") { t.Errorf("formatHeader() = %q, want to contain \"# Test Header\"", header) } } func TestMarkdownFormatter_FormatBold(t *testing.T) { tests := []struct { name string useColors bool text string wantContains string }{ { name: "without colors", useColors: false, text: "Bold Text", wantContains: "**Bold Text**", }, { name: "with colors", useColors: true, text: "Bold Text", wantContains: "Bold Text", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { formatter := &MarkdownFormatter{UseColors: tt.useColors} result := formatter.formatBold(tt.text) if !strings.Contains(result, tt.wantContains) { t.Errorf("formatBold() = %q, want to contain %q", result, tt.wantContains) } }) } } func TestMarkdownFormatter_Colorize(t *testing.T) { tests := []struct { name string useColors bool text string color string wantColor bool }{ { name: "without colors", useColors: false, text: "Test", color: colorRed, wantColor: false, }, { name: "with colors", useColors: true, text: "Test", color: colorRed, wantColor: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { formatter := &MarkdownFormatter{UseColors: tt.useColors} result := formatter.colorize(tt.text, tt.color) hasColor := strings.Contains(result, tt.color) if hasColor != tt.wantColor { t.Errorf("colorize() has color codes = %v, want %v", hasColor, tt.wantColor) } if !strings.Contains(result, tt.text) { t.Errorf("colorize() doesn't contain original text %q", tt.text) } }) } } func TestMarkdownFormatter_FormatContext(t *testing.T) { formatter := &MarkdownFormatter{UseColors: false} context := map[string]interface{}{ "schema": "public", "table": "users", "column": "id", "pattern": "^id_", "max_length": 64, } result := formatter.formatContext(context) // Should not include schema, table, column (they're in location) if strings.Contains(result, "schema") { t.Error("formatContext() should skip schema field") } if strings.Contains(result, "table=") { t.Error("formatContext() should skip table field") } if strings.Contains(result, "column=") { t.Error("formatContext() should skip column field") } // Should include other fields if !strings.Contains(result, "pattern") { t.Error("formatContext() should include pattern field") } if !strings.Contains(result, "max_length") { t.Error("formatContext() should include max_length field") } } func TestMarkdownFormatter_FormatViolation(t *testing.T) { formatter := &MarkdownFormatter{UseColors: false} violation := ValidationResult{ RuleName: "test_rule", Level: "error", Message: "Test violation message", Location: "public.users.id", Passed: false, Context: map[string]interface{}{ "pattern": "^id_", }, } result := formatter.formatViolation(violation, colorRed) if !strings.Contains(result, "test_rule") { t.Error("formatViolation() should include rule name") } if !strings.Contains(result, "Test violation message") { t.Error("formatViolation() should include message") } if !strings.Contains(result, "public.users.id") { t.Error("formatViolation() should include location") } if !strings.Contains(result, "Location:") { t.Error("formatViolation() should include Location label") } if !strings.Contains(result, "Message:") { t.Error("formatViolation() should include Message label") } } func TestReportFormatConstants(t *testing.T) { // Test that color constants are defined if colorReset == "" { t.Error("colorReset is not defined") } if colorRed == "" { t.Error("colorRed is not defined") } if colorYellow == "" { t.Error("colorYellow is not defined") } if colorGreen == "" { t.Error("colorGreen is not defined") } if colorBold == "" { t.Error("colorBold is not defined") } }