package provision import ( "context" "errors" "testing" ) func TestCreateNode_Success(t *testing.T) { h := newHarness(t) ctx := context.Background() n, err := h.svc.CreateNode(ctx, freeSpec(), "key-1") if err != nil { t.Fatalf("CreateNode: %v", err) } if n.Status != StatusProvisioning { t.Errorf("status = %s, want provisioning", n.Status) } if n.ProviderInstanceID == "" { t.Error("instance ID not recorded") } if n.Endpoint == pendingEndpoint || n.Endpoint == "" { t.Errorf("endpoint not updated: %q", n.Endpoint) } if h.tokens.calls != 1 { t.Errorf("bootstrap token issued %d times, want 1", h.tokens.calls) } if h.adapter.createCount() != 1 { t.Errorf("boot calls = %d, want 1", h.adapter.createCount()) } if h.store.countEvents(EventProvisioned) != 1 { t.Errorf("provisioned events = %d, want 1", h.store.countEvents(EventProvisioned)) } } func TestCreateNode_IdempotentReplay(t *testing.T) { h := newHarness(t) ctx := context.Background() n1, err := h.svc.CreateNode(ctx, freeSpec(), "same-key") if err != nil { t.Fatalf("first CreateNode: %v", err) } n2, err := h.svc.CreateNode(ctx, freeSpec(), "same-key") if err != nil { t.Fatalf("replay CreateNode: %v", err) } if n1.UUID != n2.UUID { t.Errorf("replay returned different node: %s vs %s", n1.UUID, n2.UUID) } if h.adapter.createCount() != 1 { t.Errorf("idempotency violated: boot called %d times, want 1", h.adapter.createCount()) } } func TestCreateNode_BootFailure(t *testing.T) { h := newHarness(t) ctx := context.Background() h.adapter.createErr = errors.New("vendor 500") _, err := h.svc.CreateNode(ctx, freeSpec(), "key-boom") if err == nil { t.Fatal("expected error on boot failure") } // Node should be destroyed. n, _ := h.store.GetNodeByUUID(ctx, h.store.idem["key-boom"]) if n == nil || n.Status != StatusDestroyed { t.Errorf("node not destroyed after boot failure: %+v", n) } if got := h.svc.FailCount(PoolConsumable); got != 1 { t.Errorf("fail count = %d, want 1", got) } if h.alert.count(AlertBootFailed) != 1 { t.Errorf("boot_failed alerts = %d, want 1", h.alert.count(AlertBootFailed)) } } func TestDestroyNode_Idempotent(t *testing.T) { h := newHarness(t) ctx := context.Background() n, err := h.svc.CreateNode(ctx, freeSpec(), "key-d") if err != nil { t.Fatalf("CreateNode: %v", err) } if err := h.svc.DestroyNode(ctx, n.ID); err != nil { t.Fatalf("DestroyNode: %v", err) } if err := h.svc.DestroyNode(ctx, n.ID); err != nil { t.Fatalf("second DestroyNode should be no-op: %v", err) } if h.adapter.destroyCount() != 1 { t.Errorf("vendor destroy called %d times, want 1 (idempotent)", h.adapter.destroyCount()) } got, _ := h.store.GetNode(ctx, n.ID) if got.Status != StatusDestroyed { t.Errorf("status = %s, want destroyed", got.Status) } } func TestRotateIP(t *testing.T) { h := newHarness(t) ctx := context.Background() n, err := h.svc.CreateNode(ctx, freeSpec(), "key-ip") if err != nil { t.Fatalf("CreateNode: %v", err) } before := h.store.version oldEndpoint := n.Endpoint updated, err := h.svc.RotateIP(ctx, n.ID) if err != nil { t.Fatalf("RotateIP: %v", err) } if updated.Endpoint == oldEndpoint { t.Errorf("endpoint not changed: still %s", updated.Endpoint) } if h.store.version <= before { t.Errorf("directory version not bumped: %d <= %d", h.store.version, before) } if h.store.countEvents(EventIPRotated) != 1 { t.Errorf("ip_rotated events = %d, want 1", h.store.countEvents(EventIPRotated)) } stored, _ := h.store.GetNode(ctx, n.ID) if stored.Endpoint != updated.Endpoint { t.Errorf("stored endpoint %s != returned %s", stored.Endpoint, updated.Endpoint) } } func TestRotateIP_Unsupported(t *testing.T) { h := newHarness(t) ctx := context.Background() h.adapter.elastic = false n, err := h.svc.CreateNode(ctx, freeSpec(), "key-ip2") if err != nil { t.Fatalf("CreateNode: %v", err) } _, err = h.svc.RotateIP(ctx, n.ID) if !errors.Is(err, ErrElasticIPUnsupported) { t.Errorf("err = %v, want ErrElasticIPUnsupported", err) } } func TestListProviders(t *testing.T) { h := newHarness(t) ctx := context.Background() ps, err := h.svc.ListProviders(ctx, PoolPremium) if err != nil { t.Fatalf("ListProviders: %v", err) } if len(ps) != 1 || ps[0].Pool != PoolPremium { t.Errorf("premium providers = %+v, want exactly 1 premium", ps) } }