Utility tool for upgrading talos nodes.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 1371 lines 42 kB view raw
1package talos 2 3import ( 4 "context" 5 "errors" 6 "testing" 7 "time" 8 9 "github.com/cosi-project/runtime/pkg/resource" 10 "github.com/siderolabs/talos/pkg/machinery/api/common" 11 "github.com/siderolabs/talos/pkg/machinery/api/machine" 12 talosclient "github.com/siderolabs/talos/pkg/machinery/client" 13 "github.com/stretchr/testify/assert" 14 "github.com/stretchr/testify/require" 15 corev1 "k8s.io/api/core/v1" 16 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 "k8s.io/client-go/kubernetes/fake" 18) 19 20// ============================================================================ 21// MockClient Tests 22// ============================================================================ 23 24func TestMockClient_DefaultBehavior(t *testing.T) { 25 mock := &MockClient{} 26 27 t.Run("Close returns nil", func(t *testing.T) { 28 err := mock.Close() 29 assert.NoError(t, err) 30 }) 31 32 t.Run("GetVersion returns default", func(t *testing.T) { 33 version, err := mock.GetVersion(context.Background(), "192.168.1.1") 34 require.NoError(t, err) 35 assert.Equal(t, "1.7.0", version) 36 }) 37 38 t.Run("GetMachineType returns unknown", func(t *testing.T) { 39 machineType, err := mock.GetMachineType(context.Background(), "192.168.1.1") 40 require.NoError(t, err) 41 assert.Equal(t, "unknown", machineType) 42 }) 43 44 t.Run("IsReachable returns true", func(t *testing.T) { 45 reachable := mock.IsReachable(context.Background(), "192.168.1.1") 46 assert.True(t, reachable) 47 }) 48 49 t.Run("Upgrade returns nil", func(t *testing.T) { 50 err := mock.Upgrade(context.Background(), "192.168.1.1", "image:v1.7.0", true) 51 assert.NoError(t, err) 52 }) 53 54 t.Run("WaitForNode returns nil", func(t *testing.T) { 55 err := mock.WaitForNode(context.Background(), "192.168.1.1", time.Minute) 56 assert.NoError(t, err) 57 }) 58 59 t.Run("GetNodeStatus returns populated status", func(t *testing.T) { 60 status := mock.GetNodeStatus(context.Background(), "192.168.1.1", "profile-a", "worker", true) 61 assert.Equal(t, "192.168.1.1", status.IP) 62 assert.Equal(t, "profile-a", status.Profile) 63 assert.Equal(t, "worker", status.Role) 64 assert.Equal(t, "1.7.0", status.Version) 65 assert.True(t, status.Secureboot) 66 assert.True(t, status.Reachable) 67 }) 68 69 t.Run("WatchUpgrade returns nil", func(t *testing.T) { 70 err := mock.WatchUpgrade(context.Background(), "192.168.1.1", time.Minute, nil) 71 assert.NoError(t, err) 72 }) 73 74 t.Run("WaitForServices returns nil", func(t *testing.T) { 75 err := mock.WaitForServices(context.Background(), "192.168.1.1", []string{"etcd", "kubelet"}, time.Minute) 76 assert.NoError(t, err) 77 }) 78 79 t.Run("WaitForStaticPods returns nil", func(t *testing.T) { 80 err := mock.WaitForStaticPods(context.Background(), "192.168.1.1", time.Minute) 81 assert.NoError(t, err) 82 }) 83} 84 85func TestMockClient_CustomFunctions(t *testing.T) { 86 t.Run("custom GetVersion", func(t *testing.T) { 87 mock := &MockClient{ 88 GetVersionFunc: func(ctx context.Context, nodeIP string) (string, error) { 89 if nodeIP == "192.168.1.1" { 90 return "1.8.0", nil 91 } 92 return "", errors.New("node not found") 93 }, 94 } 95 96 version, err := mock.GetVersion(context.Background(), "192.168.1.1") 97 require.NoError(t, err) 98 assert.Equal(t, "1.8.0", version) 99 100 _, err = mock.GetVersion(context.Background(), "192.168.1.99") 101 assert.Error(t, err) 102 }) 103 104 t.Run("custom IsReachable", func(t *testing.T) { 105 unreachableNodes := map[string]bool{"192.168.1.2": true} 106 mock := &MockClient{ 107 IsReachableFunc: func(ctx context.Context, nodeIP string) bool { 108 return !unreachableNodes[nodeIP] 109 }, 110 } 111 112 assert.True(t, mock.IsReachable(context.Background(), "192.168.1.1")) 113 assert.False(t, mock.IsReachable(context.Background(), "192.168.1.2")) 114 }) 115 116 t.Run("custom Upgrade with error", func(t *testing.T) { 117 mock := &MockClient{ 118 UpgradeFunc: func(ctx context.Context, nodeIP, image string, preserve bool) error { 119 return errors.New("upgrade failed") 120 }, 121 } 122 123 err := mock.Upgrade(context.Background(), "192.168.1.1", "image:v1.7.0", true) 124 assert.Error(t, err) 125 assert.Contains(t, err.Error(), "upgrade failed") 126 }) 127 128 t.Run("custom WaitForNode timeout", func(t *testing.T) { 129 mock := &MockClient{ 130 WaitForNodeFunc: func(ctx context.Context, nodeIP string, timeout time.Duration) error { 131 return errors.New("timeout waiting for node") 132 }, 133 } 134 135 err := mock.WaitForNode(context.Background(), "192.168.1.1", time.Minute) 136 assert.Error(t, err) 137 }) 138 139 t.Run("custom GetNodeStatus unreachable", func(t *testing.T) { 140 mock := &MockClient{ 141 GetNodeStatusFunc: func(ctx context.Context, nodeIP, profile, role string, secureboot bool) NodeStatus { 142 return NodeStatus{ 143 IP: nodeIP, 144 Profile: profile, 145 Role: role, 146 Version: "N/A", 147 Reachable: false, 148 } 149 }, 150 } 151 152 status := mock.GetNodeStatus(context.Background(), "192.168.1.1", "test", "worker", false) 153 assert.False(t, status.Reachable) 154 assert.Equal(t, "N/A", status.Version) 155 }) 156 157 t.Run("custom WatchUpgrade with callback", func(t *testing.T) { 158 var callbackCalls []UpgradeProgress 159 mock := &MockClient{ 160 WatchUpgradeFunc: func(ctx context.Context, nodeIP string, timeout time.Duration, onProgress ProgressCallback) error { 161 // Simulate progress events 162 if onProgress != nil { 163 onProgress(UpgradeProgress{Stage: "upgrading", Phase: "prepare"}) 164 onProgress(UpgradeProgress{Stage: "rebooting"}) 165 onProgress(UpgradeProgress{Stage: "running", Done: true}) 166 } 167 return nil 168 }, 169 } 170 171 err := mock.WatchUpgrade(context.Background(), "192.168.1.1", time.Minute, func(p UpgradeProgress) { 172 callbackCalls = append(callbackCalls, p) 173 }) 174 require.NoError(t, err) 175 assert.Len(t, callbackCalls, 3) 176 assert.Equal(t, "upgrading", callbackCalls[0].Stage) 177 assert.Equal(t, "running", callbackCalls[2].Stage) 178 assert.True(t, callbackCalls[2].Done) 179 }) 180 181 t.Run("custom WaitForServices with error", func(t *testing.T) { 182 mock := &MockClient{ 183 WaitForServicesFunc: func(ctx context.Context, nodeIP string, services []string, timeout time.Duration) error { 184 return errors.New("service not healthy") 185 }, 186 } 187 err := mock.WaitForServices(context.Background(), "192.168.1.1", []string{"etcd"}, time.Minute) 188 assert.Error(t, err) 189 assert.Contains(t, err.Error(), "service not healthy") 190 }) 191 192 t.Run("custom WaitForStaticPods with error", func(t *testing.T) { 193 mock := &MockClient{ 194 WaitForStaticPodsFunc: func(ctx context.Context, nodeIP string, timeout time.Duration) error { 195 return errors.New("pods not ready") 196 }, 197 } 198 err := mock.WaitForStaticPods(context.Background(), "192.168.1.1", time.Minute) 199 assert.Error(t, err) 200 assert.Contains(t, err.Error(), "pods not ready") 201 }) 202} 203 204// ============================================================================ 205// NodeStatus Tests 206// ============================================================================ 207 208func TestNodeStatus_Struct(t *testing.T) { 209 status := NodeStatus{ 210 IP: "192.168.1.1", 211 Profile: "amd64-intel", 212 Role: "controlplane", 213 Version: "1.7.0", 214 MachineType: "controlplane", 215 Secureboot: true, 216 Reachable: true, 217 } 218 219 assert.Equal(t, "192.168.1.1", status.IP) 220 assert.Equal(t, "amd64-intel", status.Profile) 221 assert.Equal(t, "controlplane", status.Role) 222 assert.Equal(t, "1.7.0", status.Version) 223 assert.Equal(t, "controlplane", status.MachineType) 224 assert.True(t, status.Secureboot) 225 assert.True(t, status.Reachable) 226} 227 228func TestNodeStatus_Unreachable(t *testing.T) { 229 status := NodeStatus{ 230 IP: "192.168.1.99", 231 Profile: "unknown", 232 Role: "worker", 233 Version: "N/A", 234 MachineType: "unknown", 235 Secureboot: false, 236 Reachable: false, 237 } 238 239 assert.Equal(t, "192.168.1.99", status.IP) 240 assert.Equal(t, "N/A", status.Version) 241 assert.False(t, status.Reachable) 242} 243 244// ============================================================================ 245// UpgradeProgress Tests 246// ============================================================================ 247 248func TestUpgradeProgress_Struct(t *testing.T) { 249 progress := UpgradeProgress{ 250 Stage: "upgrading", 251 Phase: "install", 252 Task: "downloading image", 253 Action: "START", 254 Done: false, 255 } 256 257 assert.Equal(t, "upgrading", progress.Stage) 258 assert.Equal(t, "install", progress.Phase) 259 assert.Equal(t, "downloading image", progress.Task) 260 assert.Equal(t, "START", progress.Action) 261 assert.False(t, progress.Done) 262} 263 264func TestUpgradeProgress_Done(t *testing.T) { 265 progress := UpgradeProgress{ 266 Stage: "running", 267 Done: true, 268 } 269 270 assert.True(t, progress.Done) 271} 272 273func TestUpgradeProgress_Error(t *testing.T) { 274 progress := UpgradeProgress{ 275 Stage: "upgrading", 276 Error: "failed to download image", 277 } 278 279 assert.NotEmpty(t, progress.Error) 280} 281 282// ============================================================================ 283// Interface Compliance Tests 284// ============================================================================ 285 286func TestMockClient_ImplementsInterface(t *testing.T) { 287 // This test verifies that MockClient implements TalosClientInterface 288 var client TalosClientInterface = &MockClient{} 289 assert.NotNil(t, client) 290} 291 292// ============================================================================ 293// Context Handling Tests 294// ============================================================================ 295 296func TestMockClient_ContextCancellation(t *testing.T) { 297 mock := &MockClient{ 298 WaitForNodeFunc: func(ctx context.Context, nodeIP string, timeout time.Duration) error { 299 select { 300 case <-ctx.Done(): 301 return ctx.Err() 302 case <-time.After(timeout): 303 return nil 304 } 305 }, 306 } 307 308 ctx, cancel := context.WithCancel(context.Background()) 309 cancel() // Cancel immediately 310 311 err := mock.WaitForNode(ctx, "192.168.1.1", time.Hour) 312 assert.Error(t, err) 313 assert.Equal(t, context.Canceled, err) 314} 315 316func TestMockClient_ContextTimeout(t *testing.T) { 317 mock := &MockClient{ 318 WaitForNodeFunc: func(ctx context.Context, nodeIP string, timeout time.Duration) error { 319 select { 320 case <-ctx.Done(): 321 return ctx.Err() 322 case <-time.After(time.Second): // Simulates long operation 323 return nil 324 } 325 }, 326 } 327 328 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) 329 defer cancel() 330 331 err := mock.WaitForNode(ctx, "192.168.1.1", time.Hour) 332 assert.Error(t, err) 333 assert.Equal(t, context.DeadlineExceeded, err) 334} 335 336// ============================================================================ 337// Upgrade Workflow Simulation Tests 338// ============================================================================ 339 340func TestMockClient_SimulateUpgradeWorkflow(t *testing.T) { 341 // Track upgrade state 342 upgradeStarted := false 343 nodeVersion := "1.6.0" 344 345 mock := &MockClient{ 346 GetVersionFunc: func(ctx context.Context, nodeIP string) (string, error) { 347 return nodeVersion, nil 348 }, 349 UpgradeFunc: func(ctx context.Context, nodeIP, image string, preserve bool) error { 350 upgradeStarted = true 351 return nil 352 }, 353 WatchUpgradeFunc: func(ctx context.Context, nodeIP string, timeout time.Duration, onProgress ProgressCallback) error { 354 if !upgradeStarted { 355 return errors.New("upgrade not started") 356 } 357 // Simulate upgrade completing 358 nodeVersion = "1.7.0" 359 if onProgress != nil { 360 onProgress(UpgradeProgress{Stage: "running", Done: true}) 361 } 362 return nil 363 }, 364 } 365 366 ctx := context.Background() 367 368 // Check initial version 369 version, err := mock.GetVersion(ctx, "192.168.1.1") 370 require.NoError(t, err) 371 assert.Equal(t, "1.6.0", version) 372 373 // Start upgrade 374 err = mock.Upgrade(ctx, "192.168.1.1", "image:v1.7.0", true) 375 require.NoError(t, err) 376 assert.True(t, upgradeStarted) 377 378 // Watch for completion 379 var finalProgress UpgradeProgress 380 err = mock.WatchUpgrade(ctx, "192.168.1.1", time.Minute, func(p UpgradeProgress) { 381 finalProgress = p 382 }) 383 require.NoError(t, err) 384 assert.True(t, finalProgress.Done) 385 386 // Check new version 387 version, err = mock.GetVersion(ctx, "192.168.1.1") 388 require.NoError(t, err) 389 assert.Equal(t, "1.7.0", version) 390} 391 392// ============================================================================ 393// Service List Tests 394// ============================================================================ 395 396func TestGetControlPlaneServices(t *testing.T) { 397 services := GetControlPlaneServices() 398 expected := []string{"etcd", "kubelet", "apid", "trustd"} 399 assert.Equal(t, expected, services) 400 assert.Len(t, services, 4) 401} 402 403func TestGetWorkerServices(t *testing.T) { 404 services := GetWorkerServices() 405 expected := []string{"kubelet", "apid", "trustd"} 406 assert.Equal(t, expected, services) 407 assert.Len(t, services, 3) 408} 409 410// ============================================================================ 411// parseEvent Tests 412// ============================================================================ 413 414func TestParseEvent_SequenceEvent(t *testing.T) { 415 c := &Client{} // nil talos/k8s clients are fine - parseEvent doesn't use them 416 417 t.Run("sequence start", func(t *testing.T) { 418 event := talosclient.Event{ 419 Payload: &machine.SequenceEvent{ 420 Sequence: "upgrade", 421 Action: machine.SequenceEvent_START, 422 }, 423 } 424 progress := c.parseEvent(event) 425 require.NotNil(t, progress) 426 assert.Equal(t, "upgrade", progress.Phase) 427 assert.Equal(t, "START", progress.Action) 428 assert.Empty(t, progress.Error) 429 }) 430 431 t.Run("sequence stop", func(t *testing.T) { 432 event := talosclient.Event{ 433 Payload: &machine.SequenceEvent{ 434 Sequence: "reboot", 435 Action: machine.SequenceEvent_STOP, 436 }, 437 } 438 progress := c.parseEvent(event) 439 require.NotNil(t, progress) 440 assert.Equal(t, "reboot", progress.Phase) 441 assert.Equal(t, "STOP", progress.Action) 442 }) 443 444 t.Run("sequence with error", func(t *testing.T) { 445 event := talosclient.Event{ 446 Payload: &machine.SequenceEvent{ 447 Sequence: "upgrade", 448 Action: machine.SequenceEvent_START, 449 Error: &common.Error{ 450 Message: "upgrade failed: disk full", 451 }, 452 }, 453 } 454 progress := c.parseEvent(event) 455 require.NotNil(t, progress) 456 assert.Equal(t, "upgrade failed: disk full", progress.Error) 457 }) 458} 459 460func TestParseEvent_PhaseEvent(t *testing.T) { 461 c := &Client{} 462 463 t.Run("phase start", func(t *testing.T) { 464 event := talosclient.Event{ 465 Payload: &machine.PhaseEvent{ 466 Phase: "install", 467 Action: machine.PhaseEvent_START, 468 }, 469 } 470 progress := c.parseEvent(event) 471 require.NotNil(t, progress) 472 assert.Equal(t, "install", progress.Phase) 473 assert.Equal(t, "START", progress.Action) 474 }) 475 476 t.Run("phase stop", func(t *testing.T) { 477 event := talosclient.Event{ 478 Payload: &machine.PhaseEvent{ 479 Phase: "boot", 480 Action: machine.PhaseEvent_STOP, 481 }, 482 } 483 progress := c.parseEvent(event) 484 require.NotNil(t, progress) 485 assert.Equal(t, "boot", progress.Phase) 486 assert.Equal(t, "STOP", progress.Action) 487 }) 488} 489 490func TestParseEvent_TaskEvent(t *testing.T) { 491 c := &Client{} 492 493 t.Run("task start", func(t *testing.T) { 494 event := talosclient.Event{ 495 Payload: &machine.TaskEvent{ 496 Task: "downloading image", 497 Action: machine.TaskEvent_START, 498 }, 499 } 500 progress := c.parseEvent(event) 501 require.NotNil(t, progress) 502 assert.Equal(t, "downloading image", progress.Task) 503 assert.Equal(t, "START", progress.Action) 504 }) 505 506 t.Run("task stop", func(t *testing.T) { 507 event := talosclient.Event{ 508 Payload: &machine.TaskEvent{ 509 Task: "writing disk", 510 Action: machine.TaskEvent_STOP, 511 }, 512 } 513 progress := c.parseEvent(event) 514 require.NotNil(t, progress) 515 assert.Equal(t, "writing disk", progress.Task) 516 assert.Equal(t, "STOP", progress.Action) 517 }) 518} 519 520func TestParseEvent_MachineStatusEvent(t *testing.T) { 521 c := &Client{} 522 523 t.Run("running state", func(t *testing.T) { 524 event := talosclient.Event{ 525 Payload: &machine.MachineStatusEvent{ 526 Stage: machine.MachineStatusEvent_RUNNING, 527 }, 528 } 529 progress := c.parseEvent(event) 530 require.NotNil(t, progress) 531 assert.Equal(t, "running", progress.Stage) 532 // Note: parseEvent no longer sets Done - caller decides based on context 533 assert.False(t, progress.Done) 534 }) 535 536 t.Run("booting state", func(t *testing.T) { 537 event := talosclient.Event{ 538 Payload: &machine.MachineStatusEvent{ 539 Stage: machine.MachineStatusEvent_BOOTING, 540 }, 541 } 542 progress := c.parseEvent(event) 543 require.NotNil(t, progress) 544 assert.Equal(t, "booting", progress.Stage) 545 assert.False(t, progress.Done) 546 }) 547 548 t.Run("upgrading state", func(t *testing.T) { 549 event := talosclient.Event{ 550 Payload: &machine.MachineStatusEvent{ 551 Stage: machine.MachineStatusEvent_UPGRADING, 552 }, 553 } 554 progress := c.parseEvent(event) 555 require.NotNil(t, progress) 556 assert.Equal(t, "upgrading", progress.Stage) 557 assert.False(t, progress.Done) 558 }) 559 560 t.Run("rebooting state", func(t *testing.T) { 561 event := talosclient.Event{ 562 Payload: &machine.MachineStatusEvent{ 563 Stage: machine.MachineStatusEvent_REBOOTING, 564 }, 565 } 566 progress := c.parseEvent(event) 567 require.NotNil(t, progress) 568 assert.Equal(t, "rebooting", progress.Stage) 569 assert.False(t, progress.Done) 570 }) 571} 572 573func TestParseEvent_UnknownPayload(t *testing.T) { 574 c := &Client{} 575 576 t.Run("unknown proto message returns nil", func(t *testing.T) { 577 // Use a proto message type that isn't handled in the switch 578 event := talosclient.Event{ 579 Payload: &machine.Version{}, 580 } 581 progress := c.parseEvent(event) 582 assert.Nil(t, progress) 583 }) 584 585 t.Run("nil payload returns nil", func(t *testing.T) { 586 event := talosclient.Event{ 587 Payload: nil, 588 } 589 progress := c.parseEvent(event) 590 assert.Nil(t, progress) 591 }) 592} 593 594// ============================================================================ 595// K8s Client Tests (using fake clientset) 596// ============================================================================ 597 598// makeNode creates a K8s Node for testing 599func makeNode(name, ip string, ready bool) *corev1.Node { 600 status := corev1.ConditionFalse 601 if ready { 602 status = corev1.ConditionTrue 603 } 604 return &corev1.Node{ 605 ObjectMeta: metav1.ObjectMeta{Name: name}, 606 Status: corev1.NodeStatus{ 607 Addresses: []corev1.NodeAddress{ 608 {Type: corev1.NodeInternalIP, Address: ip}, 609 }, 610 Conditions: []corev1.NodeCondition{ 611 {Type: corev1.NodeReady, Status: status}, 612 }, 613 }, 614 } 615} 616 617func TestIsK8sNodeReady(t *testing.T) { 618 ctx := context.Background() 619 620 t.Run("nil k8s client returns true", func(t *testing.T) { 621 c := &Client{k8s: nil} 622 assert.True(t, c.isK8sNodeReady(ctx, "192.168.1.1")) 623 }) 624 625 t.Run("node ready", func(t *testing.T) { 626 fakeClient := fake.NewSimpleClientset(makeNode("node1", "192.168.1.1", true)) 627 c := &Client{k8s: fakeClient} 628 assert.True(t, c.isK8sNodeReady(ctx, "192.168.1.1")) 629 }) 630 631 t.Run("node not ready", func(t *testing.T) { 632 fakeClient := fake.NewSimpleClientset(makeNode("node1", "192.168.1.1", false)) 633 c := &Client{k8s: fakeClient} 634 assert.False(t, c.isK8sNodeReady(ctx, "192.168.1.1")) 635 }) 636 637 t.Run("node not found", func(t *testing.T) { 638 fakeClient := fake.NewSimpleClientset(makeNode("node1", "192.168.1.99", true)) 639 c := &Client{k8s: fakeClient} 640 assert.False(t, c.isK8sNodeReady(ctx, "192.168.1.1")) 641 }) 642 643 t.Run("empty cluster", func(t *testing.T) { 644 fakeClient := fake.NewSimpleClientset() 645 c := &Client{k8s: fakeClient} 646 assert.False(t, c.isK8sNodeReady(ctx, "192.168.1.1")) 647 }) 648 649 t.Run("multiple nodes finds correct one", func(t *testing.T) { 650 fakeClient := fake.NewSimpleClientset( 651 makeNode("node1", "192.168.1.1", false), 652 makeNode("node2", "192.168.1.2", true), 653 makeNode("node3", "192.168.1.3", true), 654 ) 655 c := &Client{k8s: fakeClient} 656 assert.False(t, c.isK8sNodeReady(ctx, "192.168.1.1")) 657 assert.True(t, c.isK8sNodeReady(ctx, "192.168.1.2")) 658 assert.True(t, c.isK8sNodeReady(ctx, "192.168.1.3")) 659 }) 660} 661 662func TestGetK8sNodeName(t *testing.T) { 663 ctx := context.Background() 664 665 t.Run("nil k8s client returns empty", func(t *testing.T) { 666 c := &Client{k8s: nil} 667 assert.Equal(t, "", c.GetK8sNodeName(ctx, "192.168.1.1")) 668 }) 669 670 t.Run("node found", func(t *testing.T) { 671 fakeClient := fake.NewSimpleClientset(makeNode("my-node", "192.168.1.1", true)) 672 c := &Client{k8s: fakeClient} 673 assert.Equal(t, "my-node", c.GetK8sNodeName(ctx, "192.168.1.1")) 674 }) 675 676 t.Run("node not found", func(t *testing.T) { 677 fakeClient := fake.NewSimpleClientset(makeNode("node1", "192.168.1.99", true)) 678 c := &Client{k8s: fakeClient} 679 assert.Equal(t, "", c.GetK8sNodeName(ctx, "192.168.1.1")) 680 }) 681 682 t.Run("multiple nodes finds correct one", func(t *testing.T) { 683 fakeClient := fake.NewSimpleClientset( 684 makeNode("controlplane-1", "192.168.1.1", true), 685 makeNode("worker-1", "192.168.1.2", true), 686 makeNode("worker-2", "192.168.1.3", true), 687 ) 688 c := &Client{k8s: fakeClient} 689 assert.Equal(t, "controlplane-1", c.GetK8sNodeName(ctx, "192.168.1.1")) 690 assert.Equal(t, "worker-1", c.GetK8sNodeName(ctx, "192.168.1.2")) 691 assert.Equal(t, "worker-2", c.GetK8sNodeName(ctx, "192.168.1.3")) 692 assert.Equal(t, "", c.GetK8sNodeName(ctx, "192.168.1.99")) 693 }) 694} 695 696// ============================================================================ 697// SDK-Dependent Function Tests (using MockTalosMachineClient) 698// ============================================================================ 699 700func TestClient_GetVersion(t *testing.T) { 701 ctx := context.Background() 702 703 t.Run("success with v prefix", func(t *testing.T) { 704 mockTalos := &MockTalosMachineClient{ 705 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 706 return &machine.VersionResponse{ 707 Messages: []*machine.Version{ 708 {Version: &machine.VersionInfo{Tag: "v1.8.0"}}, 709 }, 710 }, nil 711 }, 712 } 713 c := &Client{talos: mockTalos} 714 version, err := c.GetVersion(ctx, "192.168.1.1") 715 require.NoError(t, err) 716 assert.Equal(t, "1.8.0", version) 717 }) 718 719 t.Run("success without v prefix", func(t *testing.T) { 720 mockTalos := &MockTalosMachineClient{ 721 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 722 return &machine.VersionResponse{ 723 Messages: []*machine.Version{ 724 {Version: &machine.VersionInfo{Tag: "1.7.5"}}, 725 }, 726 }, nil 727 }, 728 } 729 c := &Client{talos: mockTalos} 730 version, err := c.GetVersion(ctx, "192.168.1.1") 731 require.NoError(t, err) 732 assert.Equal(t, "1.7.5", version) 733 }) 734 735 t.Run("error from SDK", func(t *testing.T) { 736 mockTalos := &MockTalosMachineClient{ 737 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 738 return nil, errors.New("connection refused") 739 }, 740 } 741 c := &Client{talos: mockTalos} 742 _, err := c.GetVersion(ctx, "192.168.1.1") 743 require.Error(t, err) 744 assert.Contains(t, err.Error(), "failed to get version") 745 }) 746 747 t.Run("empty messages", func(t *testing.T) { 748 mockTalos := &MockTalosMachineClient{ 749 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 750 return &machine.VersionResponse{Messages: []*machine.Version{}}, nil 751 }, 752 } 753 c := &Client{talos: mockTalos} 754 _, err := c.GetVersion(ctx, "192.168.1.1") 755 require.Error(t, err) 756 assert.Contains(t, err.Error(), "no version in response") 757 }) 758 759 t.Run("nil version in message", func(t *testing.T) { 760 mockTalos := &MockTalosMachineClient{ 761 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 762 return &machine.VersionResponse{ 763 Messages: []*machine.Version{ 764 {Version: nil}, 765 }, 766 }, nil 767 }, 768 } 769 c := &Client{talos: mockTalos} 770 _, err := c.GetVersion(ctx, "192.168.1.1") 771 require.Error(t, err) 772 assert.Contains(t, err.Error(), "no version in response") 773 }) 774 775 t.Run("empty tag", func(t *testing.T) { 776 mockTalos := &MockTalosMachineClient{ 777 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 778 return &machine.VersionResponse{ 779 Messages: []*machine.Version{ 780 {Version: &machine.VersionInfo{Tag: ""}}, 781 }, 782 }, nil 783 }, 784 } 785 c := &Client{talos: mockTalos} 786 version, err := c.GetVersion(ctx, "192.168.1.1") 787 require.NoError(t, err) 788 assert.Equal(t, "", version) 789 }) 790} 791 792func TestClient_GetMachineType(t *testing.T) { 793 ctx := context.Background() 794 795 t.Run("returns unknown by design", func(t *testing.T) { 796 mockTalos := &MockTalosMachineClient{ 797 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 798 return &machine.VersionResponse{ 799 Messages: []*machine.Version{ 800 { 801 Version: &machine.VersionInfo{Tag: "v1.7.0"}, 802 Platform: &machine.PlatformInfo{Name: "metal"}, 803 }, 804 }, 805 }, nil 806 }, 807 } 808 c := &Client{talos: mockTalos} 809 machineType, err := c.GetMachineType(ctx, "192.168.1.1") 810 require.NoError(t, err) 811 assert.Equal(t, "unknown", machineType) 812 }) 813 814 t.Run("error from SDK", func(t *testing.T) { 815 mockTalos := &MockTalosMachineClient{ 816 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 817 return nil, errors.New("connection refused") 818 }, 819 } 820 c := &Client{talos: mockTalos} 821 _, err := c.GetMachineType(ctx, "192.168.1.1") 822 require.Error(t, err) 823 assert.Contains(t, err.Error(), "failed to get version") 824 }) 825} 826 827func TestClient_IsReachable(t *testing.T) { 828 ctx := context.Background() 829 830 t.Run("reachable node", func(t *testing.T) { 831 mockTalos := &MockTalosMachineClient{ 832 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 833 return &machine.VersionResponse{ 834 Messages: []*machine.Version{ 835 {Version: &machine.VersionInfo{Tag: "v1.7.0"}}, 836 }, 837 }, nil 838 }, 839 } 840 c := &Client{talos: mockTalos} 841 assert.True(t, c.IsReachable(ctx, "192.168.1.1")) 842 }) 843 844 t.Run("unreachable node", func(t *testing.T) { 845 mockTalos := &MockTalosMachineClient{ 846 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 847 return nil, errors.New("connection refused") 848 }, 849 } 850 c := &Client{talos: mockTalos} 851 assert.False(t, c.IsReachable(ctx, "192.168.1.1")) 852 }) 853} 854 855func TestClient_Upgrade(t *testing.T) { 856 ctx := context.Background() 857 858 t.Run("success", func(t *testing.T) { 859 mockTalos := &MockTalosMachineClient{ 860 UpgradeWithOptionsFunc: func(ctx context.Context, opts ...talosclient.UpgradeOption) (*machine.UpgradeResponse, error) { 861 return &machine.UpgradeResponse{}, nil 862 }, 863 } 864 c := &Client{talos: mockTalos} 865 err := c.Upgrade(ctx, "192.168.1.1", "ghcr.io/siderolabs/installer:v1.8.0", true) 866 require.NoError(t, err) 867 }) 868 869 t.Run("error from SDK", func(t *testing.T) { 870 mockTalos := &MockTalosMachineClient{ 871 UpgradeWithOptionsFunc: func(ctx context.Context, opts ...talosclient.UpgradeOption) (*machine.UpgradeResponse, error) { 872 return nil, errors.New("upgrade in progress") 873 }, 874 } 875 c := &Client{talos: mockTalos} 876 err := c.Upgrade(ctx, "192.168.1.1", "ghcr.io/siderolabs/installer:v1.8.0", false) 877 require.Error(t, err) 878 assert.Contains(t, err.Error(), "upgrade failed") 879 }) 880} 881 882func TestClient_Close(t *testing.T) { 883 t.Run("nil talos client", func(t *testing.T) { 884 c := &Client{talos: nil} 885 err := c.Close() 886 require.NoError(t, err) 887 }) 888 889 t.Run("close success", func(t *testing.T) { 890 mockTalos := &MockTalosMachineClient{ 891 CloseFunc: func() error { 892 return nil 893 }, 894 } 895 c := &Client{talos: mockTalos} 896 err := c.Close() 897 require.NoError(t, err) 898 }) 899 900 t.Run("close error", func(t *testing.T) { 901 mockTalos := &MockTalosMachineClient{ 902 CloseFunc: func() error { 903 return errors.New("close failed") 904 }, 905 } 906 c := &Client{talos: mockTalos} 907 err := c.Close() 908 require.Error(t, err) 909 assert.Contains(t, err.Error(), "close failed") 910 }) 911} 912 913func TestClient_GetNodeStatus(t *testing.T) { 914 ctx := context.Background() 915 916 t.Run("reachable node with version", func(t *testing.T) { 917 mockTalos := &MockTalosMachineClient{ 918 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 919 return &machine.VersionResponse{ 920 Messages: []*machine.Version{ 921 {Version: &machine.VersionInfo{Tag: "v1.8.0"}}, 922 }, 923 }, nil 924 }, 925 } 926 c := &Client{talos: mockTalos, k8s: nil} 927 status := c.GetNodeStatus(ctx, "192.168.1.1", "amd64-intel", "controlplane", true) 928 929 assert.Equal(t, "192.168.1.1", status.IP) 930 assert.Equal(t, "amd64-intel", status.Profile) 931 assert.Equal(t, "controlplane", status.Role) 932 assert.Equal(t, "1.8.0", status.Version) 933 assert.Equal(t, "unknown", status.MachineType) 934 assert.True(t, status.Secureboot) 935 assert.True(t, status.Reachable) 936 }) 937 938 t.Run("unreachable node", func(t *testing.T) { 939 mockTalos := &MockTalosMachineClient{ 940 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 941 return nil, errors.New("connection refused") 942 }, 943 } 944 c := &Client{talos: mockTalos, k8s: nil} 945 status := c.GetNodeStatus(ctx, "192.168.1.1", "amd64-intel", "worker", false) 946 947 assert.Equal(t, "192.168.1.1", status.IP) 948 assert.Equal(t, "amd64-intel", status.Profile) 949 assert.Equal(t, "worker", status.Role) 950 assert.Equal(t, "N/A", status.Version) 951 assert.Equal(t, "unknown", status.MachineType) 952 assert.False(t, status.Secureboot) 953 assert.False(t, status.Reachable) 954 }) 955} 956 957// ============================================================================ 958// MockTalosMachineClient Tests 959// ============================================================================ 960 961func TestMockTalosMachineClient_DefaultBehavior(t *testing.T) { 962 mock := &MockTalosMachineClient{} 963 ctx := context.Background() 964 965 t.Run("Close returns nil", func(t *testing.T) { 966 err := mock.Close() 967 assert.NoError(t, err) 968 }) 969 970 t.Run("Version returns default", func(t *testing.T) { 971 resp, err := mock.Version(ctx) 972 require.NoError(t, err) 973 require.NotNil(t, resp) 974 require.Len(t, resp.Messages, 1) 975 assert.Equal(t, "v1.7.0", resp.Messages[0].Version.Tag) 976 }) 977 978 t.Run("UpgradeWithOptions returns empty response", func(t *testing.T) { 979 resp, err := mock.UpgradeWithOptions(ctx) 980 require.NoError(t, err) 981 assert.NotNil(t, resp) 982 }) 983 984 t.Run("ServiceInfo returns empty slice", func(t *testing.T) { 985 resp, err := mock.ServiceInfo(ctx, "etcd") 986 require.NoError(t, err) 987 assert.Empty(t, resp) 988 }) 989 990 t.Run("EventsWatchV2 returns nil", func(t *testing.T) { 991 eventCh := make(chan talosclient.EventResult, 10) 992 err := mock.EventsWatchV2(ctx, eventCh) 993 assert.NoError(t, err) 994 }) 995 996 t.Run("COSIList returns empty list", func(t *testing.T) { 997 list, err := mock.COSIList(ctx, resource.NewMetadata("test", "type", "id", resource.VersionUndefined)) 998 require.NoError(t, err) 999 assert.Empty(t, list.Items) 1000 }) 1001} 1002 1003func TestMockTalosMachineClient_ImplementsInterface(t *testing.T) { 1004 var client TalosMachineClient = &MockTalosMachineClient{} 1005 assert.NotNil(t, client) 1006} 1007 1008// ============================================================================ 1009// Clock-Based Tests for Timeout Functions 1010// ============================================================================ 1011 1012func TestClient_WaitForNode(t *testing.T) { 1013 ctx := context.Background() 1014 1015 t.Run("returns immediately when node is reachable", func(t *testing.T) { 1016 mockTalos := &MockTalosMachineClient{ 1017 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 1018 return &machine.VersionResponse{ 1019 Messages: []*machine.Version{ 1020 {Version: &machine.VersionInfo{Tag: "v1.7.0"}}, 1021 }, 1022 }, nil 1023 }, 1024 } 1025 mockClock := NewMockClock(time.Now()) 1026 1027 c := &Client{talos: mockTalos, k8s: nil, clock: mockClock} 1028 err := c.WaitForNode(ctx, "192.168.1.1", time.Minute) 1029 require.NoError(t, err) 1030 }) 1031 1032 t.Run("times out when node is unreachable", func(t *testing.T) { 1033 mockTalos := &MockTalosMachineClient{ 1034 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 1035 return nil, errors.New("connection refused") 1036 }, 1037 } 1038 startTime := time.Now() 1039 mockClock := &MockClock{ 1040 CurrentTime: startTime, 1041 AdvanceOnAfter: true, 1042 } 1043 // Mock clock that advances time on each Sleep call 1044 sleepCount := 0 1045 mockClock.SleepFunc = func(d time.Duration) { 1046 sleepCount++ 1047 mockClock.CurrentTime = mockClock.CurrentTime.Add(d) 1048 } 1049 1050 c := &Client{talos: mockTalos, k8s: nil, clock: mockClock} 1051 err := c.WaitForNode(ctx, "192.168.1.1", 30*time.Second) 1052 require.Error(t, err) 1053 assert.Contains(t, err.Error(), "timeout waiting for node") 1054 // Verify multiple retries occurred (5 second sleep, 30 second timeout = ~6 retries) 1055 assert.GreaterOrEqual(t, sleepCount, 5, "expected at least 5 sleep calls") 1056 }) 1057 1058 t.Run("context cancellation", func(t *testing.T) { 1059 mockTalos := &MockTalosMachineClient{ 1060 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 1061 return nil, errors.New("connection refused") 1062 }, 1063 } 1064 mockClock := NewMockClock(time.Now()) 1065 1066 c := &Client{talos: mockTalos, k8s: nil, clock: mockClock} 1067 1068 canceledCtx, cancel := context.WithCancel(ctx) 1069 cancel() 1070 1071 err := c.WaitForNode(canceledCtx, "192.168.1.1", time.Minute) 1072 require.Error(t, err) 1073 assert.Equal(t, context.Canceled, err) 1074 }) 1075 1076 t.Run("node reachable but k8s not ready then becomes ready", func(t *testing.T) { 1077 mockTalos := &MockTalosMachineClient{ 1078 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 1079 return &machine.VersionResponse{ 1080 Messages: []*machine.Version{ 1081 {Version: &machine.VersionInfo{Tag: "v1.7.0"}}, 1082 }, 1083 }, nil 1084 }, 1085 } 1086 1087 // Start with unready node, then make it ready 1088 k8sReady := false 1089 fakeClient := fake.NewSimpleClientset(makeNode("node1", "192.168.1.1", false)) 1090 1091 mockClock := NewMockClock(time.Now()) 1092 mockClock.SleepFunc = func(d time.Duration) { 1093 mockClock.CurrentTime = mockClock.CurrentTime.Add(d) 1094 // After first sleep, make k8s node ready 1095 if !k8sReady { 1096 k8sReady = true 1097 // Update the fake client's node to be ready 1098 node := makeNode("node1", "192.168.1.1", true) 1099 fakeClient.CoreV1().Nodes().Update(ctx, node, metav1.UpdateOptions{}) 1100 } 1101 } 1102 1103 c := &Client{talos: mockTalos, k8s: fakeClient, clock: mockClock} 1104 err := c.WaitForNode(ctx, "192.168.1.1", time.Minute) 1105 require.NoError(t, err) 1106 }) 1107} 1108 1109func TestClient_WaitForServices(t *testing.T) { 1110 ctx := context.Background() 1111 1112 t.Run("returns immediately when all services are healthy", func(t *testing.T) { 1113 mockTalos := &MockTalosMachineClient{ 1114 ServiceInfoFunc: func(ctx context.Context, service string) ([]talosclient.ServiceInfo, error) { 1115 return []talosclient.ServiceInfo{ 1116 { 1117 Service: &machine.ServiceInfo{ 1118 Id: service, 1119 State: "Running", 1120 Health: &machine.ServiceHealth{Healthy: true}, 1121 }, 1122 }, 1123 }, nil 1124 }, 1125 } 1126 mockClock := NewMockClock(time.Now()) 1127 1128 c := &Client{talos: mockTalos, clock: mockClock} 1129 err := c.WaitForServices(ctx, "192.168.1.1", []string{"etcd", "kubelet"}, time.Minute) 1130 require.NoError(t, err) 1131 }) 1132 1133 t.Run("times out when service is unhealthy", func(t *testing.T) { 1134 mockTalos := &MockTalosMachineClient{ 1135 ServiceInfoFunc: func(ctx context.Context, service string) ([]talosclient.ServiceInfo, error) { 1136 return []talosclient.ServiceInfo{ 1137 { 1138 Service: &machine.ServiceInfo{ 1139 Id: service, 1140 State: "Running", 1141 Health: &machine.ServiceHealth{Healthy: false}, 1142 }, 1143 }, 1144 }, nil 1145 }, 1146 } 1147 startTime := time.Now() 1148 mockClock := &MockClock{CurrentTime: startTime} 1149 mockClock.SleepFunc = func(d time.Duration) { 1150 mockClock.CurrentTime = mockClock.CurrentTime.Add(d) 1151 } 1152 1153 c := &Client{talos: mockTalos, clock: mockClock} 1154 err := c.WaitForServices(ctx, "192.168.1.1", []string{"etcd"}, 10*time.Second) 1155 require.Error(t, err) 1156 assert.Contains(t, err.Error(), "timeout waiting for services") 1157 }) 1158 1159 t.Run("times out when service is not running", func(t *testing.T) { 1160 mockTalos := &MockTalosMachineClient{ 1161 ServiceInfoFunc: func(ctx context.Context, service string) ([]talosclient.ServiceInfo, error) { 1162 return []talosclient.ServiceInfo{ 1163 { 1164 Service: &machine.ServiceInfo{ 1165 Id: service, 1166 State: "Starting", 1167 Health: nil, 1168 }, 1169 }, 1170 }, nil 1171 }, 1172 } 1173 startTime := time.Now() 1174 mockClock := &MockClock{CurrentTime: startTime} 1175 mockClock.SleepFunc = func(d time.Duration) { 1176 mockClock.CurrentTime = mockClock.CurrentTime.Add(d) 1177 } 1178 1179 c := &Client{talos: mockTalos, clock: mockClock} 1180 err := c.WaitForServices(ctx, "192.168.1.1", []string{"etcd"}, 10*time.Second) 1181 require.Error(t, err) 1182 assert.Contains(t, err.Error(), "timeout waiting for services") 1183 }) 1184 1185 t.Run("times out when service info errors", func(t *testing.T) { 1186 mockTalos := &MockTalosMachineClient{ 1187 ServiceInfoFunc: func(ctx context.Context, service string) ([]talosclient.ServiceInfo, error) { 1188 return nil, errors.New("service not found") 1189 }, 1190 } 1191 startTime := time.Now() 1192 mockClock := &MockClock{CurrentTime: startTime} 1193 mockClock.SleepFunc = func(d time.Duration) { 1194 mockClock.CurrentTime = mockClock.CurrentTime.Add(d) 1195 } 1196 1197 c := &Client{talos: mockTalos, clock: mockClock} 1198 err := c.WaitForServices(ctx, "192.168.1.1", []string{"etcd"}, 10*time.Second) 1199 require.Error(t, err) 1200 assert.Contains(t, err.Error(), "timeout waiting for services") 1201 }) 1202 1203 t.Run("context cancellation", func(t *testing.T) { 1204 mockTalos := &MockTalosMachineClient{ 1205 ServiceInfoFunc: func(ctx context.Context, service string) ([]talosclient.ServiceInfo, error) { 1206 return nil, errors.New("service not found") 1207 }, 1208 } 1209 mockClock := NewMockClock(time.Now()) 1210 1211 c := &Client{talos: mockTalos, clock: mockClock} 1212 1213 canceledCtx, cancel := context.WithCancel(ctx) 1214 cancel() 1215 1216 err := c.WaitForServices(canceledCtx, "192.168.1.1", []string{"etcd"}, time.Minute) 1217 require.Error(t, err) 1218 assert.Equal(t, context.Canceled, err) 1219 }) 1220 1221 t.Run("service becomes healthy after retry", func(t *testing.T) { 1222 attempts := 0 1223 mockTalos := &MockTalosMachineClient{ 1224 ServiceInfoFunc: func(ctx context.Context, service string) ([]talosclient.ServiceInfo, error) { 1225 attempts++ 1226 if attempts < 3 { 1227 return []talosclient.ServiceInfo{ 1228 { 1229 Service: &machine.ServiceInfo{ 1230 Id: service, 1231 State: "Starting", 1232 Health: nil, 1233 }, 1234 }, 1235 }, nil 1236 } 1237 return []talosclient.ServiceInfo{ 1238 { 1239 Service: &machine.ServiceInfo{ 1240 Id: service, 1241 State: "Running", 1242 Health: &machine.ServiceHealth{Healthy: true}, 1243 }, 1244 }, 1245 }, nil 1246 }, 1247 } 1248 mockClock := NewMockClock(time.Now()) 1249 mockClock.SleepFunc = func(d time.Duration) { 1250 mockClock.CurrentTime = mockClock.CurrentTime.Add(d) 1251 } 1252 1253 c := &Client{talos: mockTalos, clock: mockClock} 1254 err := c.WaitForServices(ctx, "192.168.1.1", []string{"etcd"}, time.Minute) 1255 require.NoError(t, err) 1256 assert.Equal(t, 3, attempts) 1257 }) 1258} 1259 1260// ============================================================================ 1261// MockClock Tests 1262// ============================================================================ 1263 1264func TestMockClock(t *testing.T) { 1265 t.Run("Now returns CurrentTime", func(t *testing.T) { 1266 now := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) 1267 clock := NewMockClock(now) 1268 assert.Equal(t, now, clock.Now()) 1269 }) 1270 1271 t.Run("Advance moves time forward", func(t *testing.T) { 1272 now := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) 1273 clock := NewMockClock(now) 1274 clock.Advance(time.Hour) 1275 assert.Equal(t, now.Add(time.Hour), clock.Now()) 1276 }) 1277 1278 t.Run("Sleep advances time", func(t *testing.T) { 1279 now := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) 1280 clock := NewMockClock(now) 1281 clock.Sleep(5 * time.Second) 1282 assert.Equal(t, now.Add(5*time.Second), clock.Now()) 1283 }) 1284 1285 t.Run("After returns immediately with value", func(t *testing.T) { 1286 now := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) 1287 clock := NewMockClock(now) 1288 1289 ch := clock.After(time.Second) 1290 select { 1291 case receivedTime := <-ch: 1292 assert.Equal(t, now, receivedTime) 1293 default: 1294 assert.Fail(t, "After should return immediately in mock") 1295 } 1296 }) 1297 1298 t.Run("AdvanceOnAfter option", func(t *testing.T) { 1299 now := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) 1300 clock := &MockClock{ 1301 CurrentTime: now, 1302 AdvanceOnAfter: true, 1303 } 1304 1305 clock.After(5 * time.Second) 1306 assert.Equal(t, now.Add(5*time.Second), clock.Now()) 1307 }) 1308 1309 t.Run("After with custom AfterFunc", func(t *testing.T) { 1310 customTime := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC) 1311 clock := &MockClock{ 1312 CurrentTime: time.Now(), 1313 AfterFunc: func(d time.Duration) <-chan time.Time { 1314 ch := make(chan time.Time, 1) 1315 ch <- customTime 1316 return ch 1317 }, 1318 } 1319 1320 ch := clock.After(time.Second) 1321 select { 1322 case receivedTime := <-ch: 1323 assert.Equal(t, customTime, receivedTime) 1324 default: 1325 assert.Fail(t, "After should return immediately") 1326 } 1327 }) 1328} 1329 1330// Test nil clock fallback paths 1331func TestClient_NilClockFallback(t *testing.T) { 1332 ctx := context.Background() 1333 1334 t.Run("WaitForServices with nil clock uses real clock", func(t *testing.T) { 1335 // This test verifies the nil clock fallback path 1336 // We use a very short timeout and a mock that returns healthy immediately 1337 mockTalos := &MockTalosMachineClient{ 1338 ServiceInfoFunc: func(ctx context.Context, service string) ([]talosclient.ServiceInfo, error) { 1339 return []talosclient.ServiceInfo{ 1340 { 1341 Service: &machine.ServiceInfo{ 1342 Id: service, 1343 State: "Running", 1344 Health: &machine.ServiceHealth{Healthy: true}, 1345 }, 1346 }, 1347 }, nil 1348 }, 1349 } 1350 // Note: clock is nil here - exercises the fallback 1351 c := &Client{talos: mockTalos, clock: nil} 1352 err := c.WaitForServices(ctx, "192.168.1.1", []string{"etcd"}, time.Second) 1353 require.NoError(t, err) 1354 }) 1355 1356 t.Run("WaitForNode with nil clock uses real clock", func(t *testing.T) { 1357 mockTalos := &MockTalosMachineClient{ 1358 VersionFunc: func(ctx context.Context) (*machine.VersionResponse, error) { 1359 return &machine.VersionResponse{ 1360 Messages: []*machine.Version{ 1361 {Version: &machine.VersionInfo{Tag: "v1.7.0"}}, 1362 }, 1363 }, nil 1364 }, 1365 } 1366 // Note: clock is nil here - exercises the fallback 1367 c := &Client{talos: mockTalos, k8s: nil, clock: nil} 1368 err := c.WaitForNode(ctx, "192.168.1.1", time.Second) 1369 require.NoError(t, err) 1370 }) 1371}