Utility tool for upgrading talos nodes.
1package factory
2
3import (
4 "encoding/json"
5 "io"
6 "net/http"
7 "net/http/httptest"
8 "net/url"
9 "testing"
10
11 "github.com/evanjarrett/homelab/internal/config"
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"
14 "gopkg.in/yaml.v3"
15)
16
17// ============================================================================
18// NewClient() Tests
19// ============================================================================
20
21func TestNewClient_DefaultURL(t *testing.T) {
22 client := NewClient("")
23 assert.Equal(t, "https://factory.talos.dev", client.baseURL)
24 assert.NotNil(t, client.httpClient)
25}
26
27func TestNewClient_CustomURL(t *testing.T) {
28 client := NewClient("https://custom.factory.dev")
29 assert.Equal(t, "https://custom.factory.dev", client.baseURL)
30}
31
32// ============================================================================
33// BuildSchematic() Tests
34// ============================================================================
35
36func TestBuildSchematic_MinimalProfile(t *testing.T) {
37 profile := config.Profile{
38 Arch: "amd64",
39 Platform: "metal",
40 Extensions: []string{},
41 }
42
43 schematic := BuildSchematic(profile)
44 require.NotNil(t, schematic)
45 assert.NotNil(t, schematic.Customization)
46 assert.Nil(t, schematic.Customization.SystemExtensions)
47 assert.Empty(t, schematic.Customization.ExtraKernelArgs)
48 assert.Nil(t, schematic.Overlay)
49}
50
51func TestBuildSchematic_WithExtensions(t *testing.T) {
52 profile := config.Profile{
53 Arch: "amd64",
54 Platform: "metal",
55 Extensions: []string{
56 "siderolabs/i915",
57 "siderolabs/iscsi-tools",
58 },
59 }
60
61 schematic := BuildSchematic(profile)
62 require.NotNil(t, schematic)
63 require.NotNil(t, schematic.Customization)
64 require.NotNil(t, schematic.Customization.SystemExtensions)
65 assert.Len(t, schematic.Customization.SystemExtensions.OfficialExtensions, 2)
66 assert.Contains(t, schematic.Customization.SystemExtensions.OfficialExtensions, "siderolabs/i915")
67 assert.Contains(t, schematic.Customization.SystemExtensions.OfficialExtensions, "siderolabs/iscsi-tools")
68}
69
70func TestBuildSchematic_WithKernelArgs(t *testing.T) {
71 profile := config.Profile{
72 Arch: "amd64",
73 Platform: "metal",
74 KernelArgs: []string{
75 "amd_iommu=off",
76 "nomodeset",
77 },
78 }
79
80 schematic := BuildSchematic(profile)
81 require.NotNil(t, schematic)
82 assert.Len(t, schematic.Customization.ExtraKernelArgs, 2)
83 assert.Contains(t, schematic.Customization.ExtraKernelArgs, "amd_iommu=off")
84 assert.Contains(t, schematic.Customization.ExtraKernelArgs, "nomodeset")
85}
86
87func TestBuildSchematic_WithOverlay(t *testing.T) {
88 profile := config.Profile{
89 Arch: "arm64",
90 Platform: "metal",
91 Overlay: &config.Overlay{
92 Name: "rpi_generic",
93 Image: "siderolabs/sbc-raspberrypi",
94 },
95 }
96
97 schematic := BuildSchematic(profile)
98 require.NotNil(t, schematic)
99 require.NotNil(t, schematic.Overlay)
100 assert.Equal(t, "rpi_generic", schematic.Overlay.Name)
101 assert.Equal(t, "siderolabs/sbc-raspberrypi", schematic.Overlay.Image)
102}
103
104func TestBuildSchematic_CompleteProfile(t *testing.T) {
105 profile := config.Profile{
106 Arch: "arm64",
107 Platform: "metal",
108 Secureboot: false,
109 KernelArgs: []string{"console=ttyS0"},
110 Extensions: []string{"siderolabs/iscsi-tools"},
111 Overlay: &config.Overlay{
112 Name: "turingrk1",
113 Image: "siderolabs/sbc-rockchip",
114 },
115 }
116
117 schematic := BuildSchematic(profile)
118 require.NotNil(t, schematic)
119 require.NotNil(t, schematic.Customization)
120 require.NotNil(t, schematic.Customization.SystemExtensions)
121 require.NotNil(t, schematic.Overlay)
122
123 assert.Len(t, schematic.Customization.ExtraKernelArgs, 1)
124 assert.Len(t, schematic.Customization.SystemExtensions.OfficialExtensions, 1)
125 assert.Equal(t, "turingrk1", schematic.Overlay.Name)
126}
127
128func TestBuildSchematic_YAMLOutput(t *testing.T) {
129 profile := config.Profile{
130 Arch: "amd64",
131 Platform: "metal",
132 Extensions: []string{
133 "siderolabs/i915",
134 },
135 }
136
137 schematic := BuildSchematic(profile)
138 yamlBytes, err := yaml.Marshal(schematic)
139 require.NoError(t, err)
140
141 // Verify YAML can be unmarshalled back
142 var parsed Schematic
143 err = yaml.Unmarshal(yamlBytes, &parsed)
144 require.NoError(t, err)
145 require.NotNil(t, parsed.Customization.SystemExtensions)
146 assert.Contains(t, parsed.Customization.SystemExtensions.OfficialExtensions, "siderolabs/i915")
147}
148
149// ============================================================================
150// GetSchematicID() Tests
151// ============================================================================
152
153func TestGetSchematicID_Success(t *testing.T) {
154 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
155 // Verify request
156 assert.Equal(t, "POST", r.Method)
157 assert.Equal(t, "/schematics", r.URL.Path)
158 assert.Equal(t, "application/yaml", r.Header.Get("Content-Type"))
159
160 // Read and verify body is valid YAML
161 body, err := io.ReadAll(r.Body)
162 require.NoError(t, err)
163 var schematic Schematic
164 err = yaml.Unmarshal(body, &schematic)
165 assert.NoError(t, err)
166
167 // Return success
168 w.WriteHeader(http.StatusOK)
169 json.NewEncoder(w).Encode(SchematicResponse{ID: "abc123def456"})
170 }))
171 defer server.Close()
172
173 client := NewClient(server.URL)
174 schematic := &Schematic{
175 Customization: &SchematicCustomization{
176 SystemExtensions: &SchematicSystemExtensions{
177 OfficialExtensions: []string{"siderolabs/i915"},
178 },
179 },
180 }
181
182 id, err := client.GetSchematicID(schematic)
183 require.NoError(t, err)
184 assert.Equal(t, "abc123def456", id)
185}
186
187func TestGetSchematicID_Created201(t *testing.T) {
188 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
189 w.WriteHeader(http.StatusCreated)
190 json.NewEncoder(w).Encode(SchematicResponse{ID: "newschematic123"})
191 }))
192 defer server.Close()
193
194 client := NewClient(server.URL)
195 id, err := client.GetSchematicID(&Schematic{})
196 require.NoError(t, err)
197 assert.Equal(t, "newschematic123", id)
198}
199
200func TestGetSchematicID_ServerError(t *testing.T) {
201 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
202 w.WriteHeader(http.StatusInternalServerError)
203 }))
204 defer server.Close()
205
206 client := NewClient(server.URL)
207 id, err := client.GetSchematicID(&Schematic{})
208 assert.Error(t, err)
209 assert.Empty(t, id)
210 assert.Contains(t, err.Error(), "returned status 500")
211}
212
213func TestGetSchematicID_BadRequest(t *testing.T) {
214 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
215 w.WriteHeader(http.StatusBadRequest)
216 }))
217 defer server.Close()
218
219 client := NewClient(server.URL)
220 id, err := client.GetSchematicID(&Schematic{})
221 assert.Error(t, err)
222 assert.Empty(t, id)
223 assert.Contains(t, err.Error(), "returned status 400")
224}
225
226func TestGetSchematicID_InvalidJSON(t *testing.T) {
227 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
228 w.WriteHeader(http.StatusOK)
229 w.Write([]byte("not json"))
230 }))
231 defer server.Close()
232
233 client := NewClient(server.URL)
234 id, err := client.GetSchematicID(&Schematic{})
235 assert.Error(t, err)
236 assert.Empty(t, id)
237 assert.Contains(t, err.Error(), "failed to decode")
238}
239
240func TestGetSchematicID_EmptyID(t *testing.T) {
241 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
242 w.WriteHeader(http.StatusOK)
243 json.NewEncoder(w).Encode(SchematicResponse{ID: ""})
244 }))
245 defer server.Close()
246
247 client := NewClient(server.URL)
248 id, err := client.GetSchematicID(&Schematic{})
249 assert.Error(t, err)
250 assert.Empty(t, id)
251 assert.Contains(t, err.Error(), "empty schematic ID")
252}
253
254func TestGetSchematicID_ConnectionError(t *testing.T) {
255 client := NewClient("http://localhost:59999")
256 id, err := client.GetSchematicID(&Schematic{})
257 assert.Error(t, err)
258 assert.Empty(t, id)
259 assert.Contains(t, err.Error(), "failed to post schematic")
260}
261
262// ============================================================================
263// GetInstallerImage() Tests
264// ============================================================================
265
266func TestGetInstallerImage_Standard(t *testing.T) {
267 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
268 w.WriteHeader(http.StatusOK)
269 json.NewEncoder(w).Encode(SchematicResponse{ID: "schematic123"})
270 }))
271 defer server.Close()
272
273 client := NewClient(server.URL)
274 profile := config.Profile{
275 Arch: "amd64",
276 Platform: "metal",
277 Secureboot: false,
278 Extensions: []string{"siderolabs/i915"},
279 }
280
281 image, err := client.GetInstallerImage(profile, "1.7.0")
282 require.NoError(t, err)
283 assert.Equal(t, "factory.talos.dev/installer/schematic123:v1.7.0", image)
284}
285
286func TestGetInstallerImage_Secureboot(t *testing.T) {
287 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
288 w.WriteHeader(http.StatusOK)
289 json.NewEncoder(w).Encode(SchematicResponse{ID: "secureboot456"})
290 }))
291 defer server.Close()
292
293 client := NewClient(server.URL)
294 profile := config.Profile{
295 Arch: "amd64",
296 Platform: "metal",
297 Secureboot: true,
298 Extensions: []string{"siderolabs/i915"},
299 }
300
301 image, err := client.GetInstallerImage(profile, "1.7.0")
302 require.NoError(t, err)
303 assert.Equal(t, "factory.talos.dev/installer-secureboot/secureboot456:v1.7.0", image)
304}
305
306func TestGetInstallerImage_APIError(t *testing.T) {
307 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
308 w.WriteHeader(http.StatusInternalServerError)
309 }))
310 defer server.Close()
311
312 client := NewClient(server.URL)
313 profile := config.Profile{
314 Arch: "amd64",
315 Platform: "metal",
316 }
317
318 image, err := client.GetInstallerImage(profile, "1.7.0")
319 assert.Error(t, err)
320 assert.Empty(t, image)
321}
322
323// ============================================================================
324// GenerateFactoryURL() Tests
325// ============================================================================
326
327func TestGenerateFactoryURL_BasicProfile(t *testing.T) {
328 profile := config.Profile{
329 Arch: "amd64",
330 Platform: "metal",
331 Secureboot: false,
332 }
333
334 urlStr := GenerateFactoryURL(profile, "1.7.0", "")
335
336 parsed, err := url.Parse(urlStr)
337 require.NoError(t, err)
338
339 assert.Equal(t, "https", parsed.Scheme)
340 assert.Equal(t, "factory.talos.dev", parsed.Host)
341 assert.Equal(t, "/", parsed.Path)
342
343 params := parsed.Query()
344 assert.Equal(t, "amd64", params.Get("arch"))
345 assert.Equal(t, "metal", params.Get("platform"))
346 assert.Equal(t, "metal", params.Get("target"))
347 assert.Equal(t, "1.7.0", params.Get("version"))
348 assert.Equal(t, "auto", params.Get("bootloader"))
349 assert.Equal(t, "true", params.Get("cmdline-set"))
350 assert.Empty(t, params.Get("secureboot"))
351}
352
353func TestGenerateFactoryURL_Secureboot(t *testing.T) {
354 profile := config.Profile{
355 Arch: "amd64",
356 Platform: "metal",
357 Secureboot: true,
358 }
359
360 urlStr := GenerateFactoryURL(profile, "1.7.0", "")
361
362 parsed, err := url.Parse(urlStr)
363 require.NoError(t, err)
364
365 params := parsed.Query()
366 assert.Equal(t, "true", params.Get("secureboot"))
367 assert.Equal(t, "metal", params.Get("target"))
368}
369
370func TestGenerateFactoryURL_WithExtensions(t *testing.T) {
371 profile := config.Profile{
372 Arch: "amd64",
373 Platform: "metal",
374 Extensions: []string{
375 "siderolabs/i915",
376 "siderolabs/iscsi-tools",
377 },
378 }
379
380 urlStr := GenerateFactoryURL(profile, "1.7.0", "")
381
382 parsed, err := url.Parse(urlStr)
383 require.NoError(t, err)
384
385 params := parsed.Query()
386 extensions := params["extensions"]
387 assert.Len(t, extensions, 2)
388 assert.Contains(t, extensions, "siderolabs/i915")
389 assert.Contains(t, extensions, "siderolabs/iscsi-tools")
390}
391
392func TestGenerateFactoryURL_WithKernelArgs(t *testing.T) {
393 profile := config.Profile{
394 Arch: "amd64",
395 Platform: "metal",
396 KernelArgs: []string{
397 "amd_iommu=off",
398 "nomodeset",
399 },
400 }
401
402 urlStr := GenerateFactoryURL(profile, "1.7.0", "")
403
404 parsed, err := url.Parse(urlStr)
405 require.NoError(t, err)
406
407 params := parsed.Query()
408 cmdline := params["cmdline"]
409 assert.Len(t, cmdline, 2)
410 assert.Contains(t, cmdline, "amd_iommu=off")
411 assert.Contains(t, cmdline, "nomodeset")
412}
413
414func TestGenerateFactoryURL_SBCWithOverlay(t *testing.T) {
415 profile := config.Profile{
416 Arch: "arm64",
417 Platform: "metal",
418 Overlay: &config.Overlay{
419 Name: "rpi_generic",
420 Image: "siderolabs/sbc-raspberrypi",
421 },
422 Extensions: []string{"siderolabs/iscsi-tools"},
423 }
424
425 urlStr := GenerateFactoryURL(profile, "1.7.0", "")
426
427 parsed, err := url.Parse(urlStr)
428 require.NoError(t, err)
429
430 params := parsed.Query()
431 assert.Equal(t, "arm64", params.Get("arch"))
432 assert.Equal(t, "sbc", params.Get("target"))
433 assert.Equal(t, "rpi_generic", params.Get("board"))
434 assert.Empty(t, params.Get("secureboot")) // SBCs don't use secureboot param
435
436 // SBCs should have "-" to reset defaults, then extensions
437 extensions := params["extensions"]
438 assert.Contains(t, extensions, "-")
439 assert.Contains(t, extensions, "siderolabs/iscsi-tools")
440}
441
442func TestGenerateFactoryURL_CustomBaseURL(t *testing.T) {
443 profile := config.Profile{
444 Arch: "amd64",
445 Platform: "metal",
446 }
447
448 urlStr := GenerateFactoryURL(profile, "1.7.0", "https://custom.factory.dev")
449
450 parsed, err := url.Parse(urlStr)
451 require.NoError(t, err)
452
453 assert.Equal(t, "https", parsed.Scheme)
454 assert.Equal(t, "custom.factory.dev", parsed.Host)
455}
456
457func TestGenerateFactoryURL_CompleteProfile(t *testing.T) {
458 profile := config.Profile{
459 Arch: "amd64",
460 Platform: "metal",
461 Secureboot: true,
462 KernelArgs: []string{"console=ttyS0"},
463 Extensions: []string{
464 "siderolabs/i915",
465 "siderolabs/nut-client",
466 },
467 }
468
469 urlStr := GenerateFactoryURL(profile, "1.8.0", "")
470
471 parsed, err := url.Parse(urlStr)
472 require.NoError(t, err)
473
474 params := parsed.Query()
475 assert.Equal(t, "amd64", params.Get("arch"))
476 assert.Equal(t, "metal", params.Get("platform"))
477 assert.Equal(t, "metal", params.Get("target"))
478 assert.Equal(t, "true", params.Get("secureboot"))
479 assert.Equal(t, "1.8.0", params.Get("version"))
480
481 cmdline := params["cmdline"]
482 assert.Contains(t, cmdline, "console=ttyS0")
483
484 extensions := params["extensions"]
485 assert.Contains(t, extensions, "siderolabs/i915")
486 assert.Contains(t, extensions, "siderolabs/nut-client")
487}