Skip to content

Commit fad9166

Browse files
cbleckerclaude
andcommitted
Add comprehensive unit tests for verify-steering-election tool
- Add tests for extractYAMLHeader function with various dash counts and formats - Add tests for countWords function with different content types - Add tests for validateFileNameAndGitHubID with valid/invalid formats - Add tests for validateTemplateCompliance with required fields and sections - Add tests for findBioFiles function with year filtering logic - All tests cover both success and error cases for robust validation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent d7708a1 commit fad9166

File tree

1 file changed

+396
-0
lines changed

1 file changed

+396
-0
lines changed
Lines changed: 396 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"path/filepath"
23+
"strings"
24+
"testing"
25+
"time"
26+
)
27+
28+
func TestExtractYAMLHeader(t *testing.T) {
29+
tests := []struct {
30+
name string
31+
content string
32+
want string
33+
wantErr bool
34+
}{
35+
{
36+
name: "valid YAML header with exactly 61 dashes",
37+
content: strings.Repeat("-", 61) + "\n" +
38+
"name: Test User\n" +
39+
"ID: testuser\n" +
40+
"info:\n" +
41+
" employer: Test Corp\n" +
42+
" slack: '@testuser'\n" +
43+
strings.Repeat("-", 61) + "\n" +
44+
"## Bio content\n",
45+
want: "name: Test User\nID: testuser\ninfo:\n employer: Test Corp\n slack: '@testuser'",
46+
wantErr: false,
47+
},
48+
{
49+
name: "invalid YAML header with wrong number of dashes",
50+
content: strings.Repeat("-", 60) + "\n" +
51+
"name: Test User\n" +
52+
strings.Repeat("-", 60) + "\n",
53+
want: "",
54+
wantErr: true,
55+
},
56+
{
57+
name: "no YAML header",
58+
content: "Just some content without header",
59+
want: "",
60+
wantErr: true,
61+
},
62+
{
63+
name: "YAML header with extra spaces",
64+
content: strings.Repeat("-", 61) + " \n" +
65+
"name: Test User\n" +
66+
"ID: testuser\n" +
67+
strings.Repeat("-", 61) + " \n" +
68+
"## Bio content\n",
69+
want: "name: Test User\nID: testuser",
70+
wantErr: false,
71+
},
72+
}
73+
74+
for _, tt := range tests {
75+
t.Run(tt.name, func(t *testing.T) {
76+
got, err := extractYAMLHeader(tt.content)
77+
if (err != nil) != tt.wantErr {
78+
t.Errorf("extractYAMLHeader() error = %v, wantErr %v", err, tt.wantErr)
79+
return
80+
}
81+
if got != tt.want {
82+
t.Errorf("extractYAMLHeader() = %q, want %q", got, tt.want)
83+
}
84+
})
85+
}
86+
}
87+
88+
func TestCountWords(t *testing.T) {
89+
tests := []struct {
90+
name string
91+
content string
92+
want int
93+
}{
94+
{
95+
name: "simple sentence",
96+
content: "Hello world test",
97+
want: 3,
98+
},
99+
{
100+
name: "text with newlines",
101+
content: "Hello\nworld\ntest",
102+
want: 3,
103+
},
104+
{
105+
name: "text with multiple spaces",
106+
content: "Hello world test",
107+
want: 3,
108+
},
109+
{
110+
name: "empty content",
111+
content: "",
112+
want: 0,
113+
},
114+
{
115+
name: "only whitespace",
116+
content: " \n\t \n ",
117+
want: 0,
118+
},
119+
{
120+
name: "mixed content with punctuation",
121+
content: "Hello, world! This is a test.",
122+
want: 6,
123+
},
124+
}
125+
126+
for _, tt := range tests {
127+
t.Run(tt.name, func(t *testing.T) {
128+
// Create a temporary file with the content
129+
tmpfile, err := os.CreateTemp("", "test")
130+
if err != nil {
131+
t.Fatal(err)
132+
}
133+
defer os.Remove(tmpfile.Name())
134+
135+
if _, err := tmpfile.Write([]byte(tt.content)); err != nil {
136+
t.Fatal(err)
137+
}
138+
if err := tmpfile.Close(); err != nil {
139+
t.Fatal(err)
140+
}
141+
142+
got, err := countWords(tmpfile.Name())
143+
if err != nil {
144+
t.Errorf("countWords() error = %v", err)
145+
return
146+
}
147+
if got != tt.want {
148+
t.Errorf("countWords() = %v, want %v", got, tt.want)
149+
}
150+
})
151+
}
152+
}
153+
154+
func TestValidateFileNameAndGitHubID(t *testing.T) {
155+
tests := []struct {
156+
name string
157+
filename string
158+
fileContent string
159+
wantErr bool
160+
errContains string
161+
}{
162+
{
163+
name: "valid filename and matching GitHub ID",
164+
filename: "candidate-testuser.md",
165+
fileContent: strings.Repeat("-", 61) + "\n" +
166+
"name: Test User\n" +
167+
"ID: testuser\n" +
168+
"info:\n" +
169+
" employer: Test Corp\n" +
170+
" slack: '@testuser'\n" +
171+
strings.Repeat("-", 61) + "\n" +
172+
"## Bio content\n",
173+
wantErr: false,
174+
},
175+
{
176+
name: "invalid filename format",
177+
filename: "invalid-format.md",
178+
fileContent: "dummy content",
179+
wantErr: true,
180+
errContains: "filename must follow format",
181+
},
182+
{
183+
name: "mismatched GitHub ID",
184+
filename: "candidate-testuser.md",
185+
fileContent: strings.Repeat("-", 61) + "\n" +
186+
"name: Test User\n" +
187+
"ID: differentuser\n" +
188+
"info:\n" +
189+
" employer: Test Corp\n" +
190+
" slack: '@testuser'\n" +
191+
strings.Repeat("-", 61) + "\n" +
192+
"## Bio content\n",
193+
wantErr: true,
194+
errContains: "does not match GitHub ID",
195+
},
196+
{
197+
name: "missing ID field",
198+
filename: "candidate-testuser.md",
199+
fileContent: strings.Repeat("-", 61) + "\nname: Test User\n" + strings.Repeat("-", 61) + "\n",
200+
wantErr: true,
201+
errContains: "missing 'ID' field",
202+
},
203+
}
204+
205+
for _, tt := range tests {
206+
t.Run(tt.name, func(t *testing.T) {
207+
// Create a temporary file with the content
208+
tmpfile, err := os.CreateTemp("", tt.filename)
209+
if err != nil {
210+
t.Fatal(err)
211+
}
212+
defer os.Remove(tmpfile.Name())
213+
214+
// Rename to the desired filename
215+
dir := filepath.Dir(tmpfile.Name())
216+
targetPath := filepath.Join(dir, tt.filename)
217+
if err := os.Rename(tmpfile.Name(), targetPath); err != nil {
218+
t.Fatal(err)
219+
}
220+
defer os.Remove(targetPath)
221+
222+
if err := os.WriteFile(targetPath, []byte(tt.fileContent), 0644); err != nil {
223+
t.Fatal(err)
224+
}
225+
226+
err = validateFileNameAndGitHubID(targetPath)
227+
if (err != nil) != tt.wantErr {
228+
t.Errorf("validateFileNameAndGitHubID() error = %v, wantErr %v", err, tt.wantErr)
229+
return
230+
}
231+
if tt.wantErr && tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
232+
t.Errorf("validateFileNameAndGitHubID() error = %v, should contain %q", err, tt.errContains)
233+
}
234+
})
235+
}
236+
}
237+
238+
func TestValidateTemplateCompliance(t *testing.T) {
239+
validContent := strings.Repeat("-", 61) + "\n" +
240+
"name: Test User\n" +
241+
"ID: testuser\n" +
242+
"info:\n" +
243+
" employer: Test Corp\n" +
244+
" slack: '@testuser'\n" +
245+
strings.Repeat("-", 61) + "\n" +
246+
"## What I have done\n" +
247+
"Some accomplishments\n" +
248+
"## What I'll do\n" +
249+
"Future plans\n" +
250+
"## SIGS\n" +
251+
"SIG involvement\n" +
252+
"## Resources About Me\n" +
253+
"Links and info\n"
254+
255+
tests := []struct {
256+
name string
257+
content string
258+
wantErr bool
259+
errContains string
260+
}{
261+
{
262+
name: "valid template compliance",
263+
content: validContent,
264+
wantErr: false,
265+
},
266+
{
267+
name: "missing required field - name",
268+
content: strings.Repeat("-", 61) + "\n" +
269+
"ID: testuser\n" +
270+
"info:\n" +
271+
" employer: Test Corp\n" +
272+
" slack: '@testuser'\n" +
273+
strings.Repeat("-", 61) + "\n" +
274+
"## What I have done\n## What I'll do\n## SIGS\n## Resources About Me\n",
275+
wantErr: true,
276+
errContains: "missing required field: name",
277+
},
278+
{
279+
name: "missing required section",
280+
content: strings.Repeat("-", 61) + "\n" +
281+
"name: Test User\n" +
282+
"ID: testuser\n" +
283+
"info:\n" +
284+
" employer: Test Corp\n" +
285+
" slack: '@testuser'\n" +
286+
strings.Repeat("-", 61) + "\n" +
287+
"## What I have done\n## What I'll do\n## Resources About Me\n",
288+
wantErr: true,
289+
errContains: "missing required section: SIGs",
290+
},
291+
{
292+
name: "alternative SIGs section name",
293+
content: strings.Repeat("-", 61) + "\n" +
294+
"name: Test User\n" +
295+
"ID: testuser\n" +
296+
"info:\n" +
297+
" employer: Test Corp\n" +
298+
" slack: '@testuser'\n" +
299+
strings.Repeat("-", 61) + "\n" +
300+
"## What I have done\n## What I'll do\n## SIGs\n## Resources About Me\n",
301+
wantErr: false,
302+
},
303+
}
304+
305+
for _, tt := range tests {
306+
t.Run(tt.name, func(t *testing.T) {
307+
// Create a temporary file with the content
308+
tmpfile, err := os.CreateTemp("", "test")
309+
if err != nil {
310+
t.Fatal(err)
311+
}
312+
defer os.Remove(tmpfile.Name())
313+
314+
if err := os.WriteFile(tmpfile.Name(), []byte(tt.content), 0644); err != nil {
315+
t.Fatal(err)
316+
}
317+
318+
err = validateTemplateCompliance(tmpfile.Name())
319+
if (err != nil) != tt.wantErr {
320+
t.Errorf("validateTemplateCompliance() error = %v, wantErr %v", err, tt.wantErr)
321+
return
322+
}
323+
if tt.wantErr && tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
324+
t.Errorf("validateTemplateCompliance() error = %v, should contain %q", err, tt.errContains)
325+
}
326+
})
327+
}
328+
}
329+
330+
func TestFindBioFiles(t *testing.T) {
331+
// Create a temporary directory structure
332+
tmpDir, err := os.MkdirTemp("", "elections")
333+
if err != nil {
334+
t.Fatal(err)
335+
}
336+
defer os.RemoveAll(tmpDir)
337+
338+
// Create steering directory structure
339+
steeringDir := filepath.Join(tmpDir, "steering")
340+
if err := os.MkdirAll(steeringDir, 0755); err != nil {
341+
t.Fatal(err)
342+
}
343+
344+
// Create test files
345+
currentYear := time.Now().Year()
346+
testFiles := []struct {
347+
path string
348+
shouldFind bool
349+
}{
350+
{filepath.Join(steeringDir, "2025", "candidate-user1.md"), false}, // Before startYear (2026)
351+
{filepath.Join(steeringDir, "2026", "candidate-user2.md"), true}, // At startYear
352+
{filepath.Join(steeringDir, fmt.Sprintf("%d", currentYear+1), "candidate-user3.md"), true}, // Future year
353+
{filepath.Join(steeringDir, "2026", "not-candidate.md"), false}, // Wrong filename format
354+
{filepath.Join(steeringDir, "2026", "candidate-user4.txt"), false}, // Wrong extension
355+
{filepath.Join(steeringDir, fmt.Sprintf("%d", currentYear+10), "candidate-user5.md"), false}, // Too far in future
356+
}
357+
358+
var expectedFiles []string
359+
for _, tf := range testFiles {
360+
dir := filepath.Dir(tf.path)
361+
if err := os.MkdirAll(dir, 0755); err != nil {
362+
t.Fatal(err)
363+
}
364+
if err := os.WriteFile(tf.path, []byte("dummy content"), 0644); err != nil {
365+
t.Fatal(err)
366+
}
367+
if tf.shouldFind {
368+
expectedFiles = append(expectedFiles, tf.path)
369+
}
370+
}
371+
372+
// Test findBioFiles
373+
bioFiles, err := findBioFiles(tmpDir)
374+
if err != nil {
375+
t.Errorf("findBioFiles() error = %v", err)
376+
return
377+
}
378+
379+
if len(bioFiles) != len(expectedFiles) {
380+
t.Errorf("findBioFiles() found %d files, expected %d", len(bioFiles), len(expectedFiles))
381+
}
382+
383+
// Check that all expected files are found
384+
for _, expected := range expectedFiles {
385+
found := false
386+
for _, actual := range bioFiles {
387+
if actual == expected {
388+
found = true
389+
break
390+
}
391+
}
392+
if !found {
393+
t.Errorf("findBioFiles() did not find expected file: %s", expected)
394+
}
395+
}
396+
}

0 commit comments

Comments
 (0)