package validation import ( "context" "fmt" "strings" "testing" "time" ) // mockProvider implements Validatable for testing. type mockProvider struct { name string checks []Check } func (m *mockProvider) ProviderName() string { return m.name } func (m *mockProvider) Checks(ctx context.Context) []Check { return m.checks } // mockCheck implements Check for testing. type mockCheck struct { name string category CheckCategory result CheckResult } func (m *mockCheck) Name() string { return m.name } func (m *mockCheck) Category() CheckCategory { return m.category } func (m *mockCheck) Run(ctx context.Context) CheckResult { return m.result } func TestCheckResultPassed(t *testing.T) { tests := []struct { status Status expected bool }{ {Pass, true}, {Warn, true}, {Fail, false}, {Skip, false}, {Error, false}, } for _, tc := range tests { r := CheckResult{Status: tc.status} if r.Passed() != tc.expected { t.Errorf("CheckResult{Status: %s}.Passed() = %v, want %v", tc.status, r.Passed(), tc.expected) } } } func TestCheckResultIcon(t *testing.T) { tests := []struct { status Status expected string }{ {Pass, "✓"}, {Fail, "✗"}, {Warn, "!"}, {Skip, "—"}, {Error, "⚠"}, {Status("unknown"), "?"}, } for _, tc := range tests { r := CheckResult{Status: tc.status} if r.Icon() != tc.expected { t.Errorf("CheckResult{Status: %s}.Icon() = %q, want %q", tc.status, r.Icon(), tc.expected) } } } func TestCheckFunc(t *testing.T) { ctx := context.Background() cf := CheckFunc{ CategoryField: CategoryCredentials, NameField: "test-check", RunFunc: func(ctx context.Context) CheckResult { return CheckResult{ Name: "test-check", Category: CategoryCredentials, Status: Pass, Message: "check passed", } }, } if cf.Name() != "test-check" { t.Errorf("CheckFunc.Name() = %q, want %q", cf.Name(), "test-check") } if cf.Category() != CategoryCredentials { t.Errorf("CheckFunc.Category() = %q, want %q", cf.Category(), CategoryCredentials) } result := cf.Run(ctx) if result.Status != Pass { t.Errorf("CheckFunc.Run().Status = %v, want %v", result.Status, Pass) } } func TestReportSummary(t *testing.T) { report := &Report{ Provider: "TestProvider", Results: []CheckResult{ {Status: Pass, Name: "check1"}, {Status: Pass, Name: "check2"}, {Status: Fail, Name: "check3"}, {Status: Warn, Name: "check4"}, {Status: Skip, Name: "check5"}, }, } summary := report.Summary() if summary[Pass] != 2 { t.Errorf("Summary[Pass] = %d, want 2", summary[Pass]) } if summary[Fail] != 1 { t.Errorf("Summary[Fail] = %d, want 1", summary[Fail]) } if summary[Warn] != 1 { t.Errorf("Summary[Warn] = %d, want 1", summary[Warn]) } if summary[Skip] != 1 { t.Errorf("Summary[Skip] = %d, want 1", summary[Skip]) } } func TestReportHasFailures(t *testing.T) { tests := []struct { name string results []CheckResult expected bool }{ { name: "all_pass", results: []CheckResult{{Status: Pass}, {Status: Pass}}, expected: false, }, { name: "one_fail", results: []CheckResult{{Status: Pass}, {Status: Fail}}, expected: true, }, { name: "one_error", results: []CheckResult{{Status: Pass}, {Status: Pass}, {Status: Error}}, expected: true, }, { name: "with_warn_and_skip", results: []CheckResult{{Status: Pass}, {Status: Warn}, {Status: Skip}}, expected: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { report := &Report{Results: tc.results} if report.HasFailures() != tc.expected { t.Errorf("HasFailures() = %v, want %v", report.HasFailures(), tc.expected) } }) } } func TestReportByCategory(t *testing.T) { report := &Report{ Provider: "TestProvider", Results: []CheckResult{ {Name: "token-auth", Category: CategoryCredentials, Status: Pass, Message: "token valid"}, {Name: "api-reach", Category: CategoryConnectivity, Status: Pass, Message: "API reachable"}, {Name: "ssh-keys", Category: CategorySSH, Status: Fail, Message: "no SSH keys"}, {Name: "token-format", Category: CategoryCredentials, Status: Pass, Message: "format OK"}, }, } byCat := report.ByCategory() if len(byCat[CategoryCredentials]) != 2 { t.Errorf("ByCategory[Credentials] has %d items, want 2", len(byCat[CategoryCredentials])) } if len(byCat[CategorySSH]) != 1 { t.Errorf("ByCategory[SSH] has %d items, want 1", len(byCat[CategorySSH])) } if len(byCat[CategoryQuota]) != 0 { t.Errorf("ByCategory[Quota] has %d items, want 0", len(byCat[CategoryQuota])) } } func TestReportFormat(t *testing.T) { report := &Report{ Provider: "TestProvider", Results: []CheckResult{ {Name: "token-auth", Category: CategoryCredentials, Status: Pass, Message: "Token authenticated"}, {Name: "ssh-keys", Category: CategorySSH, Status: Fail, Message: "No SSH keys configured"}, {Name: "regions", Category: CategoryServer, Status: Warn, Message: "Region not set, using default"}, }, TotalDuration: 150 * time.Millisecond, } output := report.Format() // Verify key elements appear in output if !containsAll(output, "TestProvider", "token-auth", "Token authenticated", "✓") { t.Errorf("Format() missing expected output elements") } if !containsAll(output, "No SSH keys configured", "✗") { t.Errorf("Format() missing FAIL elements") } if !containsAll(output, "Region not set", "!") { t.Errorf("Format() missing WARN elements (icon '!')") } // Check for summary line if !strings.Contains(output, "Total:") { t.Errorf("Format() missing 'Total:' in output") } } func containsAll(s string, substrs ...string) bool { for _, sub := range substrs { if !contains(s, sub) { return false } } return true } func contains(s, sub string) bool { return len(s) >= len(sub) && (s == sub || len(sub) == 0 || containsSubstring(s, sub)) } func containsSubstring(s, sub string) bool { for i := 0; i <= len(s)-len(sub); i++ { if s[i:i+len(sub)] == sub { return true } } return false } func TestRunner(t *testing.T) { ctx := context.Background() provider := &mockProvider{ name: "MockProvider", checks: []Check{ &mockCheck{ name: "token-format", category: CategoryCredentials, result: CheckResult{Status: Pass, Message: "Token format valid"}, }, &mockCheck{ name: "token-auth", category: CategoryCredentials, result: CheckResult{Status: Pass, Message: "Token authenticated"}, }, &mockCheck{ name: "ssh-keys", category: CategorySSH, result: CheckResult{Status: Fail, Message: "No SSH keys found"}, }, }, } runner := NewRunner(provider) report := runner.Run(ctx) if report.Provider != "MockProvider" { t.Errorf("Report.Provider = %q, want %q", report.Provider, "MockProvider") } if len(report.Results) != 3 { t.Errorf("Report has %d results, want 3", len(report.Results)) } if !report.HasFailures() { t.Error("Report.HasFailures() = false, expected true (one check failed)") } // Verify check names are set on results for i, r := range report.Results { if r.Name == "" { t.Errorf("Result[%d].Name is empty, should be set from Check", i) } if r.Category == "" { t.Errorf("Result[%d].Category is empty, should be set from Check", i) } } } func TestRunnerTimeout(t *testing.T) { ctx := context.Background() // Check that times out timeoutCheck := &mockCheck{ name: "slow-check", category: CategoryConnectivity, result: CheckResult{Status: Error, Message: "context deadline exceeded"}, } provider := &mockProvider{ name: "TimeoutProvider", checks: []Check{timeoutCheck}, } runner := NewRunner(provider) runner.SetTimeout(50 * time.Millisecond) report := runner.Run(ctx) if len(report.Results) != 1 { t.Fatalf("Expected 1 result, got %d", len(report.Results)) } if report.Results[0].Status != Error { t.Errorf("Expected Error status, got %s", report.Results[0].Status) } } func TestRunnerEmptyChecks(t *testing.T) { ctx := context.Background() provider := &mockProvider{ name: "EmptyProvider", checks: []Check{}, } runner := NewRunner(provider) report := runner.Run(ctx) if len(report.Results) != 0 { t.Errorf("Expected 0 results, got %d", len(report.Results)) } if report.HasFailures() { t.Error("Empty report should not have failures") } } func TestContextCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() // Cancel immediately // A check that would fail if context wasn't properly handled check := CheckFunc{ NameField: "canceled-check", CategoryField: CategoryCredentials, RunFunc: func(ctx context.Context) CheckResult { select { case <-ctx.Done(): return CheckResult{Status: Error, Message: "context canceled"} default: return CheckResult{Status: Pass, Message: "check passed"} } }, } provider := &mockProvider{ name: "CanceledProvider", checks: []Check{&check}, } runner := NewRunner(provider) report := runner.Run(ctx) if len(report.Results) != 1 { t.Fatalf("Expected 1 result, got %d", len(report.Results)) } // The result depends on when the check runs — if after cancellation, it's Error // This test verifies the context is properly propagated if report.Results[0].Name != "canceled-check" { t.Errorf("Expected name 'canceled-check', got %q", report.Results[0].Name) } } func TestReportAllPassed(t *testing.T) { tests := []struct { name string results []CheckResult expected bool }{ { name: "all_pass", results: []CheckResult{{Status: Pass}, {Status: Pass}, {Status: Pass}}, expected: true, }, { name: "pass_with_warns", results: []CheckResult{{Status: Pass}, {Status: Warn}, {Status: Pass}}, expected: true, }, { name: "pass_with_skips", results: []CheckResult{{Status: Pass}, {Status: Skip}, {Status: Pass}}, expected: true, }, { name: "pass_with_one_fail", results: []CheckResult{{Status: Pass}, {Status: Fail}, {Status: Pass}}, expected: false, }, { name: "pass_with_one_error", results: []CheckResult{{Status: Pass}, {Status: Error}, {Status: Pass}}, expected: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { report := &Report{Results: tc.results} if report.AllPassed() != tc.expected { t.Errorf("AllPassed() = %v, want %v", report.AllPassed(), tc.expected) } }) } } // ExampleCheck demonstrates creating a custom check. func ExampleCheckFunc() { ctx := context.Background() tokenFormatCheck := CheckFunc{ CategoryField: CategoryCredentials, NameField: "token-format", RunFunc: func(ctx context.Context) CheckResult { // In a real check, you'd validate the token format here return CheckResult{ Name: "token-format", Category: CategoryCredentials, Status: Pass, Message: "Token format is valid", Detail: "32 characters, alphanumeric", } }, } // Run the check result := tokenFormatCheck.Run(ctx) fmt.Printf("%s: %s\n", result.Status, result.Message) // Output: PASS: Token format is valid } // Integration test: full runner with realistic mock func TestRunnerIntegration(t *testing.T) { ctx := context.Background() // Simulate a realistic provider with multiple check categories provider := &mockProvider{ name: "Hetzner Cloud", checks: []Check{ // Credentials &mockCheck{ name: "token-format", category: CategoryCredentials, result: CheckResult{Status: Pass, Message: "Token format valid"}, }, &mockCheck{ name: "token-auth", category: CategoryCredentials, result: CheckResult{Status: Pass, Message: "Token authenticated", Detail: "Account: acme-corp"}, }, // Connectivity &mockCheck{ name: "api-reachability", category: CategoryConnectivity, result: CheckResult{Status: Pass, Message: "API endpoint reachable", Detail: "Latency: 45ms"}, }, // SSH Keys &mockCheck{ name: "ssh-keys", category: CategorySSH, result: CheckResult{Status: Fail, Message: "No SSH keys registered in account"}, }, // Server Config &mockCheck{ name: "location", category: CategoryServer, result: CheckResult{Status: Pass, Message: "Location 'fsn1' is valid"}, }, &mockCheck{ name: "server-type", category: CategoryServer, result: CheckResult{Status: Pass, Message: "Server type 'cpx21' is available"}, }, // Quota &mockCheck{ name: "server-quota", category: CategoryQuota, result: CheckResult{Status: Warn, Message: "Near quota limit", Detail: "48/50 servers used"}, }, }, } runner := NewRunner(provider) report := runner.Run(ctx) // Verify if report.Provider != "Hetzner Cloud" { t.Errorf("Provider name = %q, want 'Hetzner Cloud'", report.Provider) } if len(report.Results) != 7 { t.Errorf("Expected 7 checks, got %d", len(report.Results)) } if !report.HasFailures() { t.Error("Expected failures (SSH check should fail)") } // Check each category is represented cats := make(map[CheckCategory]bool) for _, r := range report.Results { cats[r.Category] = true } expectedCats := []CheckCategory{ CategoryCredentials, CategoryConnectivity, CategorySSH, CategoryServer, CategoryQuota, } for _, cat := range expectedCats { if !cats[cat] { t.Errorf("Missing category: %s", cat) } } // Verify output is not empty output := report.Format() if len(output) == 0 { t.Error("Format() produced empty output") } // Verify specific elements in output containsCheck(t, output, "Hetzner Cloud") containsCheck(t, output, "token-auth") containsCheck(t, output, "No SSH keys") containsCheck(t, output, "Near quota") containsCheck(t, output, "FAIL") } func containsCheck(t *testing.T, s, substr string) { t.Helper() if !strings.Contains(s, substr) { t.Errorf("Expected substring %q not found in output", substr) } }