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\n ID: testuser\n info:\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\n ID: 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\n world\n test" ,
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 ) + "\n name: 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