|
3 | 3 | package cli |
4 | 4 |
|
5 | 5 | import ( |
| 6 | + "encoding/json" |
| 7 | + "io" |
| 8 | + "os" |
6 | 9 | "testing" |
7 | 10 |
|
8 | 11 | "github.com/stretchr/testify/assert" |
| 12 | + "github.com/stretchr/testify/require" |
9 | 13 | ) |
10 | 14 |
|
11 | 15 | func TestHealthConfigValidation(t *testing.T) { |
12 | 16 | tests := []struct { |
13 | | - name string |
14 | | - config HealthConfig |
15 | | - shouldErr bool |
| 17 | + name string |
| 18 | + config HealthConfig |
| 19 | + wantDaysErr bool |
| 20 | + errContains string |
16 | 21 | }{ |
17 | 22 | { |
18 | | - name: "valid 7 days", |
19 | | - config: HealthConfig{ |
20 | | - Days: 7, |
21 | | - Threshold: 80.0, |
22 | | - }, |
23 | | - shouldErr: false, |
| 23 | + name: "valid 7 days", |
| 24 | + config: HealthConfig{Days: 7, Threshold: 80.0}, |
| 25 | + wantDaysErr: false, |
24 | 26 | }, |
25 | 27 | { |
26 | | - name: "valid 30 days", |
27 | | - config: HealthConfig{ |
28 | | - Days: 30, |
29 | | - Threshold: 80.0, |
30 | | - }, |
31 | | - shouldErr: false, |
| 28 | + name: "valid 30 days", |
| 29 | + config: HealthConfig{Days: 30, Threshold: 80.0}, |
| 30 | + wantDaysErr: false, |
32 | 31 | }, |
33 | 32 | { |
34 | | - name: "valid 90 days", |
35 | | - config: HealthConfig{ |
36 | | - Days: 90, |
37 | | - Threshold: 80.0, |
38 | | - }, |
39 | | - shouldErr: false, |
| 33 | + name: "valid 90 days", |
| 34 | + config: HealthConfig{Days: 90, Threshold: 80.0}, |
| 35 | + wantDaysErr: false, |
40 | 36 | }, |
41 | 37 | { |
42 | | - name: "invalid days value", |
43 | | - config: HealthConfig{ |
44 | | - Days: 15, |
45 | | - Threshold: 80.0, |
46 | | - }, |
47 | | - shouldErr: true, |
| 38 | + name: "zero days - validation error", |
| 39 | + config: HealthConfig{Days: 0, Threshold: 80.0}, |
| 40 | + wantDaysErr: true, |
| 41 | + errContains: "invalid days value: 0", |
| 42 | + }, |
| 43 | + { |
| 44 | + name: "negative days - validation error", |
| 45 | + config: HealthConfig{Days: -1, Threshold: 80.0}, |
| 46 | + wantDaysErr: true, |
| 47 | + errContains: "invalid days value: -1", |
| 48 | + }, |
| 49 | + { |
| 50 | + name: "days 15 - validation error", |
| 51 | + config: HealthConfig{Days: 15, Threshold: 80.0}, |
| 52 | + wantDaysErr: true, |
| 53 | + errContains: "invalid days value: 15", |
| 54 | + }, |
| 55 | + { |
| 56 | + name: "days 91 - validation error", |
| 57 | + config: HealthConfig{Days: 91, Threshold: 80.0}, |
| 58 | + wantDaysErr: true, |
| 59 | + errContains: "invalid days value: 91", |
| 60 | + }, |
| 61 | + { |
| 62 | + name: "days 365 - validation error", |
| 63 | + config: HealthConfig{Days: 365, Threshold: 80.0}, |
| 64 | + wantDaysErr: true, |
| 65 | + errContains: "invalid days value: 365", |
48 | 66 | }, |
49 | 67 | } |
50 | 68 |
|
51 | 69 | for _, tt := range tests { |
52 | 70 | t.Run(tt.name, func(t *testing.T) { |
53 | | - // For now, just validate the days parameter directly |
54 | | - // since the full RunHealth needs GitHub API access |
55 | | - if tt.config.Days != 7 && tt.config.Days != 30 && tt.config.Days != 90 { |
56 | | - assert.True(t, tt.shouldErr, "Should error for invalid days value") |
| 71 | + err := RunHealth(tt.config) |
| 72 | + if tt.wantDaysErr { |
| 73 | + require.Error(t, err, "RunHealth should return a validation error for: %s", tt.name) |
| 74 | + assert.Contains(t, err.Error(), tt.errContains, "Error message should describe the validation failure") |
57 | 75 | } else { |
58 | | - assert.False(t, tt.shouldErr, "Should not error for valid days value") |
| 76 | + // Valid days values pass days validation; any error comes from GitHub API access |
| 77 | + if err != nil { |
| 78 | + assert.NotContains(t, err.Error(), "invalid days value", "Valid days should not produce a days validation error") |
| 79 | + } |
59 | 80 | } |
60 | 81 | }) |
61 | 82 | } |
62 | 83 | } |
63 | 84 |
|
| 85 | +func TestRunHealthInvalidDays(t *testing.T) { |
| 86 | + tests := []struct { |
| 87 | + name string |
| 88 | + days int |
| 89 | + errContains string |
| 90 | + }{ |
| 91 | + {name: "zero", days: 0, errContains: "invalid days value: 0"}, |
| 92 | + {name: "negative", days: -1, errContains: "invalid days value: -1"}, |
| 93 | + {name: "too large 91", days: 91, errContains: "invalid days value: 91"}, |
| 94 | + {name: "too large 365", days: 365, errContains: "invalid days value: 365"}, |
| 95 | + {name: "not a valid option 15", days: 15, errContains: "invalid days value: 15"}, |
| 96 | + {name: "not a valid option 8", days: 8, errContains: "invalid days value: 8"}, |
| 97 | + } |
| 98 | + |
| 99 | + for _, tt := range tests { |
| 100 | + t.Run(tt.name, func(t *testing.T) { |
| 101 | + config := HealthConfig{Days: tt.days, Threshold: 80.0} |
| 102 | + err := RunHealth(config) |
| 103 | + require.Error(t, err, "RunHealth should return an error for days=%d", tt.days) |
| 104 | + assert.Contains(t, err.Error(), tt.errContains, "Error should describe the invalid days value") |
| 105 | + assert.Contains(t, err.Error(), "Must be 7, 30, or 90", "Error should list the valid days options") |
| 106 | + }) |
| 107 | + } |
| 108 | +} |
| 109 | + |
64 | 110 | func TestHealthCommand(t *testing.T) { |
65 | 111 | cmd := NewHealthCommand() |
66 | 112 |
|
67 | | - assert.NotNil(t, cmd, "Health command should be created") |
| 113 | + require.NotNil(t, cmd, "Health command should be created") |
68 | 114 | assert.Equal(t, "health", cmd.Name(), "Command name should be 'health'") |
69 | 115 | assert.True(t, cmd.HasAvailableFlags(), "Command should have flags") |
70 | 116 | assert.Contains(t, cmd.Long, "Warnings when success rate drops below threshold", "Health help should consistently use warnings terminology") |
71 | 117 |
|
72 | 118 | // Check that required flags are registered |
73 | 119 | daysFlag := cmd.Flags().Lookup("days") |
74 | | - assert.NotNil(t, daysFlag, "Should have --days flag") |
| 120 | + require.NotNil(t, daysFlag, "Should have --days flag") |
75 | 121 | assert.Equal(t, "7", daysFlag.DefValue, "Default days should be 7") |
76 | 122 |
|
77 | 123 | thresholdFlag := cmd.Flags().Lookup("threshold") |
78 | | - assert.NotNil(t, thresholdFlag, "Should have --threshold flag") |
| 124 | + require.NotNil(t, thresholdFlag, "Should have --threshold flag") |
79 | 125 | assert.Equal(t, "80", thresholdFlag.DefValue, "Default threshold should be 80") |
80 | 126 |
|
81 | 127 | jsonFlag := cmd.Flags().Lookup("json") |
82 | | - assert.NotNil(t, jsonFlag, "Should have --json flag") |
| 128 | + require.NotNil(t, jsonFlag, "Should have --json flag") |
83 | 129 |
|
84 | 130 | repoFlag := cmd.Flags().Lookup("repo") |
85 | | - assert.NotNil(t, repoFlag, "Should have --repo flag") |
| 131 | + require.NotNil(t, repoFlag, "Should have --repo flag") |
| 132 | +} |
| 133 | + |
| 134 | +func TestDisplayDetailedHealthJSON(t *testing.T) { |
| 135 | + tests := []struct { |
| 136 | + name string |
| 137 | + runs []WorkflowRun |
| 138 | + wantZeroRuns bool |
| 139 | + }{ |
| 140 | + { |
| 141 | + name: "nil runs - empty JSON structure", |
| 142 | + runs: nil, |
| 143 | + wantZeroRuns: true, |
| 144 | + }, |
| 145 | + { |
| 146 | + name: "empty runs slice - empty JSON structure", |
| 147 | + runs: []WorkflowRun{}, |
| 148 | + wantZeroRuns: true, |
| 149 | + }, |
| 150 | + } |
| 151 | + |
| 152 | + for _, tt := range tests { |
| 153 | + t.Run(tt.name, func(t *testing.T) { |
| 154 | + config := HealthConfig{ |
| 155 | + WorkflowName: "test-workflow", |
| 156 | + Days: 7, |
| 157 | + Threshold: 80.0, |
| 158 | + JSONOutput: true, |
| 159 | + } |
| 160 | + |
| 161 | + // Redirect stdout to capture JSON output |
| 162 | + oldStdout := os.Stdout |
| 163 | + r, w, err := os.Pipe() |
| 164 | + require.NoError(t, err, "os.Pipe should not fail") |
| 165 | + os.Stdout = w |
| 166 | + |
| 167 | + runErr := displayDetailedHealth(tt.runs, config) |
| 168 | + |
| 169 | + w.Close() |
| 170 | + os.Stdout = oldStdout |
| 171 | + |
| 172 | + require.NoError(t, runErr, "displayDetailedHealth should not return an error for %s", tt.name) |
| 173 | + |
| 174 | + outputBytes, readErr := io.ReadAll(r) |
| 175 | + require.NoError(t, readErr, "Reading captured output should not fail") |
| 176 | + output := string(outputBytes) |
| 177 | + |
| 178 | + require.NotEmpty(t, output, "JSON output should not be empty") |
| 179 | + |
| 180 | + var health WorkflowHealth |
| 181 | + require.NoError(t, json.Unmarshal([]byte(output), &health), "Output should be valid JSON") |
| 182 | + |
| 183 | + assert.Equal(t, "test-workflow", health.WorkflowName, "WorkflowName should match config") |
| 184 | + if tt.wantZeroRuns { |
| 185 | + assert.Equal(t, 0, health.TotalRuns, "TotalRuns should be zero for empty input") |
| 186 | + assert.Equal(t, 0, health.SuccessCount, "SuccessCount should be zero for empty input") |
| 187 | + } |
| 188 | + }) |
| 189 | + } |
86 | 190 | } |
0 commit comments