package schema import ( "strings" "testing" "git.cloudinit.dev/coreci/orca/internal/jobspec" ) func TestUpdateValidator_ValidRolling(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 4, Update: &jobspec.UpdateBlock{ Strategy: "rolling", MaxParallel: 2, MinHealthyTime: "30s", HealthyDeadline: "5m", }, } v := UpdateValidator{} if err := v.Validate(spec); err != nil { t.Fatalf("expected nil, got %v", err) } } func TestUpdateValidator_ValidCanary(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 4, Update: &jobspec.UpdateBlock{ Strategy: "canary", MaxParallel: 2, MinHealthyTime: "30s", HealthyDeadline: "5m", Canary: "10%", AutoPromote: true, }, } if err := (UpdateValidator{}).Validate(spec); err != nil { t.Fatalf("expected nil, got %v", err) } } func TestUpdateValidator_ValidBlueGreen(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 3, Update: &jobspec.UpdateBlock{ Strategy: "blue-green", MinHealthyTime: "1m", HealthyDeadline: "10m", }, } if err := (UpdateValidator{}).Validate(spec); err != nil { t.Fatalf("expected nil, got %v", err) } } func TestUpdateValidator_ValidCanaryIntegerCount(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 4, Update: &jobspec.UpdateBlock{ Strategy: "canary", Canary: "1", }, } if err := (UpdateValidator{}).Validate(spec); err != nil { t.Fatalf("integer canary count 1 should be valid, got %v", err) } } func TestUpdateValidator_ValidCanaryZeroPercent(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 4, Update: &jobspec.UpdateBlock{ Strategy: "canary", Canary: "0%", }, } if err := (UpdateValidator{}).Validate(spec); err != nil { t.Fatalf("0%% canary should be valid, got %v", err) } } func TestUpdateValidator_ValidCanaryHundredPercent(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 4, Update: &jobspec.UpdateBlock{ Strategy: "canary", Canary: "100%", }, } if err := (UpdateValidator{}).Validate(spec); err != nil { t.Fatalf("100%% canary should be valid, got %v", err) } } func TestUpdateValidator_EmptyDurationsOK(t *testing.T) { // Empty min_healthy_time / healthy_deadline should be accepted // (they are optional; defaults are applied by the executor). spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 2, Update: &jobspec.UpdateBlock{ Strategy: "rolling", }, } if err := (UpdateValidator{}).Validate(spec); err != nil { t.Fatalf("empty durations should be valid, got %v", err) } } func TestUpdateValidator_NilSpec(t *testing.T) { if err := (UpdateValidator{}).Validate(nil); err == nil { t.Fatal("expected error for nil spec") } } func TestUpdateValidator_NilUpdateBlock(t *testing.T) { spec := &jobspec.WorkloadSpec{Kind: "Service", Name: "web", Count: 2} err := (UpdateValidator{}).Validate(spec) if err == nil { t.Fatal("expected error for nil update block") } if !strings.Contains(err.Error(), "update block is nil") { t.Errorf("error = %q, want 'update block is nil'", err.Error()) } } func TestUpdateValidator_InvalidStrategy(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 2, Update: &jobspec.UpdateBlock{ Strategy: "recreate", }, } err := (UpdateValidator{}).Validate(spec) if err == nil { t.Fatal("expected error for invalid strategy") } if !strings.Contains(err.Error(), "strategy") || !strings.Contains(err.Error(), "invalid") { t.Errorf("error = %q, want 'strategy ... invalid'", err.Error()) } } func TestUpdateValidator_EmptyStrategy(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 2, Update: &jobspec.UpdateBlock{ Strategy: "", }, } err := (UpdateValidator{}).Validate(spec) if err == nil { t.Fatal("expected error for empty strategy") } if !strings.Contains(err.Error(), "strategy required") { t.Errorf("error = %q, want 'strategy required'", err.Error()) } } func TestUpdateValidator_MaxParallelZero(t *testing.T) { // max_parallel=0 means "unset" → default 1; accepted. spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 2, Update: &jobspec.UpdateBlock{ Strategy: "rolling", MaxParallel: 0, }, } if err := (UpdateValidator{}).Validate(spec); err != nil { t.Fatalf("max_parallel=0 (unset) should be valid, got %v", err) } } func TestUpdateValidator_MaxParallelNegative(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 2, Update: &jobspec.UpdateBlock{ Strategy: "rolling", MaxParallel: -1, }, } err := (UpdateValidator{}).Validate(spec) if err == nil { t.Fatal("expected error for negative max_parallel") } if !strings.Contains(err.Error(), "max_parallel") { t.Errorf("error = %q, want 'max_parallel'", err.Error()) } } func TestUpdateValidator_MaxParallelExceedsCount(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 2, Update: &jobspec.UpdateBlock{ Strategy: "rolling", MaxParallel: 5, }, } err := (UpdateValidator{}).Validate(spec) if err == nil { t.Fatal("expected error for max_parallel > count") } if !strings.Contains(err.Error(), "exceeds count") { t.Errorf("error = %q, want 'exceeds count'", err.Error()) } } func TestUpdateValidator_MaxParallelEqualsCount(t *testing.T) { // max_parallel == count is the upper bound; valid. spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 3, Update: &jobspec.UpdateBlock{ Strategy: "rolling", MaxParallel: 3, }, } if err := (UpdateValidator{}).Validate(spec); err != nil { t.Fatalf("max_parallel==count should be valid, got %v", err) } } func TestUpdateValidator_InvalidMinHealthyTime(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 2, Update: &jobspec.UpdateBlock{ Strategy: "rolling", MinHealthyTime: "not-a-duration", }, } err := (UpdateValidator{}).Validate(spec) if err == nil { t.Fatal("expected error for invalid min_healthy_time") } if !strings.Contains(err.Error(), "min_healthy_time") { t.Errorf("error = %q, want 'min_healthy_time'", err.Error()) } } func TestUpdateValidator_InvalidHealthyDeadline(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 2, Update: &jobspec.UpdateBlock{ Strategy: "rolling", HealthyDeadline: "nope", }, } err := (UpdateValidator{}).Validate(spec) if err == nil { t.Fatal("expected error for invalid healthy_deadline") } if !strings.Contains(err.Error(), "healthy_deadline") { t.Errorf("error = %q, want 'healthy_deadline'", err.Error()) } } func TestUpdateValidator_CanaryNegative(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 4, Update: &jobspec.UpdateBlock{ Strategy: "canary", Canary: "-1", }, } err := (UpdateValidator{}).Validate(spec) if err == nil { t.Fatal("expected error for negative canary count") } if !strings.Contains(err.Error(), "canary") { t.Errorf("error = %q, want 'canary'", err.Error()) } } func TestUpdateValidator_CanaryExceedsCount(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 4, Update: &jobspec.UpdateBlock{ Strategy: "canary", Canary: "5", }, } err := (UpdateValidator{}).Validate(spec) if err == nil { t.Fatal("expected error for canary > count") } if !strings.Contains(err.Error(), "exceeds count") { t.Errorf("error = %q, want 'exceeds count'", err.Error()) } } func TestUpdateValidator_CanaryPercentOver100(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 4, Update: &jobspec.UpdateBlock{ Strategy: "canary", Canary: "150%", }, } err := (UpdateValidator{}).Validate(spec) if err == nil { t.Fatal("expected error for canary > 100%") } if !strings.Contains(err.Error(), "out of range") { t.Errorf("error = %q, want 'out of range'", err.Error()) } } func TestUpdateValidator_CanaryPercentNegative(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 4, Update: &jobspec.UpdateBlock{ Strategy: "canary", Canary: "-10%", }, } err := (UpdateValidator{}).Validate(spec) if err == nil { t.Fatal("expected error for negative canary percent") } if !strings.Contains(err.Error(), "out of range") { t.Errorf("error = %q, want 'out of range'", err.Error()) } } func TestUpdateValidator_CanaryNotANumber(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 4, Update: &jobspec.UpdateBlock{ Strategy: "canary", Canary: "abc", }, } err := (UpdateValidator{}).Validate(spec) if err == nil { t.Fatal("expected error for non-numeric canary") } if !strings.Contains(err.Error(), "not a valid") { t.Errorf("error = %q, want 'not a valid'", err.Error()) } } func TestUpdateValidator_CanaryPercentNotANumber(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 4, Update: &jobspec.UpdateBlock{ Strategy: "canary", Canary: "xx%", }, } err := (UpdateValidator{}).Validate(spec) if err == nil { t.Fatal("expected error for non-numeric canary percent") } if !strings.Contains(err.Error(), "not a valid percentage") { t.Errorf("error = %q, want 'not a valid percentage'", err.Error()) } } func TestUpdateValidator_CanaryCountZeroOK(t *testing.T) { // canary=0 is the lower bound; valid. spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 4, Update: &jobspec.UpdateBlock{ Strategy: "canary", Canary: "0", }, } if err := (UpdateValidator{}).Validate(spec); err != nil { t.Fatalf("canary=0 should be valid, got %v", err) } } func TestUpdateValidator_CanaryPercentWithoutCountOK(t *testing.T) { // A percentage canary does not depend on count; valid even when // count is 0 (e.g. DaemonSet-shaped spec bypassing the Service // validator — defensive). spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 0, Update: &jobspec.UpdateBlock{ Strategy: "canary", Canary: "25%", }, } if err := (UpdateValidator{}).Validate(spec); err != nil { t.Fatalf("percentage canary with count=0 should be valid, got %v", err) } } func TestUpdateValidator_MultipleErrors(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Count: 2, Update: &jobspec.UpdateBlock{ Strategy: "recreate", MaxParallel: 99, MinHealthyTime: "nope", HealthyDeadline: "also-nope", Canary: "200%", }, } err := (UpdateValidator{}).Validate(spec) if err == nil { t.Fatal("expected error, got nil") } for _, want := range []string{ "strategy", "max_parallel", "min_healthy_time", "healthy_deadline", "canary", } { if !strings.Contains(err.Error(), want) { t.Errorf("error %q missing %q", err.Error(), want) } } } // Compile-time assertion that UpdateValidator implements Validator. var _ Validator = UpdateValidator{}