diff --git a/tests/bdd/PLAN.md b/tests/bdd/PLAN.md index 08048bb61..6285d9da0 100644 --- a/tests/bdd/PLAN.md +++ b/tests/bdd/PLAN.md @@ -129,6 +129,7 @@ refactor in every consumer; that is a feature. | `Then file {string} should exist` | | | `Then yaml file {string} key {string} should equal {string}` | Reads the YAML file, walks the dotted key path, compares to the value (with `${VAR}` expansion). | | `Then yaml file {string} key {string} should not be empty` | Same key resolution; passes if the resolved value is non-empty. Use for non-deterministic outputs (cluster IDs, identity sources) where exact-value assertions are wrong. | +| `Then yaml file {string} should have non-empty keys:` (table) | Requires a `key` header and one or more dotted key paths. Each key must exist and resolve to a non-empty value; failures distinguish a missing key from an empty value. | | `Then yaml file {string} should match:` (docstring) | Parses the docstring as YAML, parses the file as YAML, and asserts strict equality of the two trees. Every key in expected must exist in actual and vice versa. `${VAR}` expansion applies to the docstring before parsing. See "YAML comparison semantics" below for tree rules. | | `Then yaml file {string} key {string} should match:` (docstring) | Same as above but compares only the subtree at the dotted key path. | | `Then yaml file {string} should contain:` (docstring) | Subset variant: every key in expected must exist in actual with the same value; extra keys in actual are allowed. Use this when the file has dynamic or future-additive fields. `${VAR}` expansion applies. | diff --git a/tests/bdd/dsl/yamledit.go b/tests/bdd/dsl/yamledit.go index 18340055e..2002eccf4 100644 --- a/tests/bdd/dsl/yamledit.go +++ b/tests/bdd/dsl/yamledit.go @@ -97,6 +97,25 @@ func ReadYAMLKey(path, dottedKey string) (string, bool, error) { return fmt.Sprint(value), true, nil } +// RequireNonEmptyYAMLKeys asserts that every dotted key exists in path and +// resolves to a non-empty scalar representation. The first failing error names +// the table row and distinguishes a missing key from an empty value. +func RequireNonEmptyYAMLKeys(path string, keys []string) error { + for index, key := range keys { + got, found, err := ReadYAMLKey(path, key) + if err != nil { + return fmt.Errorf("row %d key %q: %w", index+1, key, err) + } + if !found { + return fmt.Errorf("row %d: %s key %q is missing (%s)", index+1, path, key, DescribeMissingKey(path, key)) + } + if got == "" { + return fmt.Errorf("row %d: %s key %q is empty", index+1, path, key) + } + } + return nil +} + // MatchYAMLSubtree compares the subtree at keyPath inside the YAML file // at filePath to the parsed expectedYAML. Empty keyPath compares against // the whole file. The expected docstring runs through Interpolate before diff --git a/tests/bdd/dsl/yamledit_test.go b/tests/bdd/dsl/yamledit_test.go index fbaf892c3..18fdd0bdc 100644 --- a/tests/bdd/dsl/yamledit_test.go +++ b/tests/bdd/dsl/yamledit_test.go @@ -113,6 +113,34 @@ list: } } +func TestRequireNonEmptyYAMLKeys(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "registration.yaml") + writeFile(t, path, `clusterID: cluster-123 +clusterGroupID: "" +selfManaged: + identitySource: psat +`) + + if err := RequireNonEmptyYAMLKeys(path, []string{"clusterID", "selfManaged.identitySource"}); err != nil { + t.Fatalf("non-empty keys: %v", err) + } + + t.Run("missing key", func(t *testing.T) { + err := RequireNonEmptyYAMLKeys(path, []string{"clusterID", "missingID"}) + if err == nil || !strings.Contains(err.Error(), `row 2`) || !strings.Contains(err.Error(), `key "missingID" is missing`) { + t.Fatalf("err = %v, want row-specific missing-key error", err) + } + }) + + t.Run("empty value", func(t *testing.T) { + err := RequireNonEmptyYAMLKeys(path, []string{"clusterGroupID"}) + if err == nil || !strings.Contains(err.Error(), `row 1`) || !strings.Contains(err.Error(), `key "clusterGroupID" is empty`) { + t.Fatalf("err = %v, want row-specific empty-value error", err) + } + }) +} + func TestMatchYAMLSubtreeExactAndSubset(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "profile.yaml") diff --git a/tests/bdd/features/multi-cluster-eks-helmfile.feature b/tests/bdd/features/multi-cluster-eks-helmfile.feature index 38d760d15..0d7aa9a77 100644 --- a/tests/bdd/features/multi-cluster-eks-helmfile.feature +++ b/tests/bdd/features/multi-cluster-eks-helmfile.feature @@ -325,8 +325,10 @@ Feature: Install a multi-cluster NVCF stack across two pre-provisioned EKS clust selfManaged: identitySource: psat """ - And yaml file "deploy/stacks/nvcf-compute-plane/registration/${EKS_COMPUTE_CLUSTER_NAME}-register-values.yaml" key "clusterID" should not be empty - And yaml file "deploy/stacks/nvcf-compute-plane/registration/${EKS_COMPUTE_CLUSTER_NAME}-register-values.yaml" key "clusterGroupID" should not be empty + And yaml file "deploy/stacks/nvcf-compute-plane/registration/${EKS_COMPUTE_CLUSTER_NAME}-register-values.yaml" should have non-empty keys: + | key | + | clusterID | + | clusterGroupID | # The register-values URLs stay as cluster register's bare-ELB # output. Gateway HTTPRoute matching is handled by the chart-native diff --git a/tests/bdd/features/multi-cluster-helmfile.feature b/tests/bdd/features/multi-cluster-helmfile.feature index 98e1206d6..05b229f39 100644 --- a/tests/bdd/features/multi-cluster-helmfile.feature +++ b/tests/bdd/features/multi-cluster-helmfile.feature @@ -202,8 +202,10 @@ Feature: Install a local multi-cluster NVCF stack with Helmfile selfManaged: identitySource: psat """ - And yaml file "deploy/stacks/nvcf-compute-plane/registration/ncp-local-compute-1-register-values.yaml" key "clusterID" should not be empty - And yaml file "deploy/stacks/nvcf-compute-plane/registration/ncp-local-compute-1-register-values.yaml" key "clusterGroupID" should not be empty + And yaml file "deploy/stacks/nvcf-compute-plane/registration/ncp-local-compute-1-register-values.yaml" should have non-empty keys: + | key | + | clusterID | + | clusterGroupID | And the "nvcr-pull-secret" image pull secret exists in namespaces: | nvca-operator | diff --git a/tests/bdd/features/multi-cluster-up.feature b/tests/bdd/features/multi-cluster-up.feature index ce7496626..021186302 100644 --- a/tests/bdd/features/multi-cluster-up.feature +++ b/tests/bdd/features/multi-cluster-up.feature @@ -157,8 +157,8 @@ Feature: Bring up a local multi-cluster NVCF stack with the CLI And file "deploy/stacks/nvcf-compute-plane/out/ncp-local-compute-1-register-values.yaml" should exist # Subset match (should contain, not should match) because the # values file carries non-deterministic IDs alongside the - # deterministic block. The IDs are asserted by individual - # should-not-be-empty steps below. + # deterministic block. The generated values are asserted as + # non-empty keys below. And yaml file "deploy/stacks/nvcf-compute-plane/out/ncp-local-compute-1-register-values.yaml" should contain: """ clusterName: ncp-local-compute-1 @@ -169,9 +169,11 @@ Feature: Bring up a local multi-cluster NVCF stack with the CLI revalServiceURL: http://reval.localhost:8080 natsURL: nats://nats.localhost:4222 """ - And yaml file "deploy/stacks/nvcf-compute-plane/out/ncp-local-compute-1-register-values.yaml" key "clusterID" should not be empty - And yaml file "deploy/stacks/nvcf-compute-plane/out/ncp-local-compute-1-register-values.yaml" key "clusterGroupID" should not be empty - And yaml file "deploy/stacks/nvcf-compute-plane/out/ncp-local-compute-1-register-values.yaml" key "selfManaged.identitySource" should not be empty + And yaml file "deploy/stacks/nvcf-compute-plane/out/ncp-local-compute-1-register-values.yaml" should have non-empty keys: + | key | + | clusterID | + | clusterGroupID | + | selfManaged.identitySource | @nvca-registration Scenario: Operator installs the first compute plane diff --git a/tests/bdd/features/observability-all.feature b/tests/bdd/features/observability-all.feature index 20c104681..b697418b2 100644 --- a/tests/bdd/features/observability-all.feature +++ b/tests/bdd/features/observability-all.feature @@ -81,8 +81,10 @@ Feature: Install local Helmfile observability for both planes """ Then the command exit code should be 0 And file "deploy/stacks/nvcf-compute-plane/registration/ncp-local-register-values.yaml" should exist - And yaml file "deploy/stacks/nvcf-compute-plane/registration/ncp-local-register-values.yaml" key "clusterID" should not be empty - And yaml file "deploy/stacks/nvcf-compute-plane/registration/ncp-local-register-values.yaml" key "clusterGroupID" should not be empty + And yaml file "deploy/stacks/nvcf-compute-plane/registration/ncp-local-register-values.yaml" should have non-empty keys: + | key | + | clusterID | + | clusterGroupID | When I run command: """ diff --git a/tests/bdd/features/observability-compute.feature b/tests/bdd/features/observability-compute.feature index be55e928f..5bc986fcd 100644 --- a/tests/bdd/features/observability-compute.feature +++ b/tests/bdd/features/observability-compute.feature @@ -90,8 +90,10 @@ Feature: Install local Helmfile observability with the compute profile """ Then the command exit code should be 0 And file "deploy/stacks/nvcf-compute-plane/registration/ncp-local-compute-1-register-values.yaml" should exist - And yaml file "deploy/stacks/nvcf-compute-plane/registration/ncp-local-compute-1-register-values.yaml" key "clusterID" should not be empty - And yaml file "deploy/stacks/nvcf-compute-plane/registration/ncp-local-compute-1-register-values.yaml" key "clusterGroupID" should not be empty + And yaml file "deploy/stacks/nvcf-compute-plane/registration/ncp-local-compute-1-register-values.yaml" should have non-empty keys: + | key | + | clusterID | + | clusterGroupID | When I run command: """ diff --git a/tests/bdd/features/single-cluster-eks-helmfile.feature b/tests/bdd/features/single-cluster-eks-helmfile.feature index e5fab01ea..00117111a 100644 --- a/tests/bdd/features/single-cluster-eks-helmfile.feature +++ b/tests/bdd/features/single-cluster-eks-helmfile.feature @@ -251,8 +251,10 @@ Feature: Install a single-cluster NVCF stack on a pre-provisioned EKS cluster wi selfManaged: identitySource: psat """ - And yaml file "deploy/stacks/nvcf-compute-plane/registration/${EKS_CLUSTER_NAME}-register-values.yaml" key "clusterID" should not be empty - And yaml file "deploy/stacks/nvcf-compute-plane/registration/${EKS_CLUSTER_NAME}-register-values.yaml" key "clusterGroupID" should not be empty + And yaml file "deploy/stacks/nvcf-compute-plane/registration/${EKS_CLUSTER_NAME}-register-values.yaml" should have non-empty keys: + | key | + | clusterID | + | clusterGroupID | # The register-values URLs stay as cluster register's bare-ELB # output (http://${EKS_GATEWAY_ADDR}, nats://${EKS_GATEWAY_ADDR}:4222). diff --git a/tests/bdd/features/single-cluster-helmfile.feature b/tests/bdd/features/single-cluster-helmfile.feature index 7efeccd26..d97a4ae0a 100644 --- a/tests/bdd/features/single-cluster-helmfile.feature +++ b/tests/bdd/features/single-cluster-helmfile.feature @@ -129,8 +129,10 @@ Feature: Install a local single-cluster NVCF stack with Helmfile revalServiceURL: http://reval.localhost:8080 natsURL: nats://nats.localhost:4222 """ - And yaml file "deploy/stacks/nvcf-compute-plane/registration/ncp-local-register-values.yaml" key "clusterID" should not be empty - And yaml file "deploy/stacks/nvcf-compute-plane/registration/ncp-local-register-values.yaml" key "clusterGroupID" should not be empty + And yaml file "deploy/stacks/nvcf-compute-plane/registration/ncp-local-register-values.yaml" should have non-empty keys: + | key | + | clusterID | + | clusterGroupID | When I run command: """ diff --git a/tests/bdd/features/single-cluster-up.feature b/tests/bdd/features/single-cluster-up.feature index 57192a65d..2eddb0254 100644 --- a/tests/bdd/features/single-cluster-up.feature +++ b/tests/bdd/features/single-cluster-up.feature @@ -174,8 +174,8 @@ Feature: Bring up a local single-cluster NVCF stack with the CLI # Subset match (should contain, not should match) because the # values file carries non-deterministic IDs alongside the - # deterministic block. The IDs are asserted by individual - # should-not-be-empty steps below. + # deterministic block. The generated values are asserted as + # non-empty keys below. # # The values file uses in-cluster URLs because compute-plane # register resolves the URL layer from the kube-context: when @@ -195,6 +195,8 @@ Feature: Bring up a local single-cluster NVCF stack with the CLI revalServiceURL: http://reval.nvcf.svc.cluster.local:8080 natsURL: nats://nats.nats-system.svc.cluster.local:4222 """ - And yaml file "deploy/stacks/nvcf-compute-plane/out/ncp-local-register-values.yaml" key "clusterID" should not be empty - And yaml file "deploy/stacks/nvcf-compute-plane/out/ncp-local-register-values.yaml" key "clusterGroupID" should not be empty - And yaml file "deploy/stacks/nvcf-compute-plane/out/ncp-local-register-values.yaml" key "selfManaged.identitySource" should not be empty + And yaml file "deploy/stacks/nvcf-compute-plane/out/ncp-local-register-values.yaml" should have non-empty keys: + | key | + | clusterID | + | clusterGroupID | + | selfManaged.identitySource | diff --git a/tests/bdd/steps/assertion_steps.go b/tests/bdd/steps/assertion_steps.go index c91f704b0..dee314534 100644 --- a/tests/bdd/steps/assertion_steps.go +++ b/tests/bdd/steps/assertion_steps.go @@ -37,6 +37,7 @@ func registerAssertionSteps(ctx *godog.ScenarioContext, sc *ScenarioContext) { ctx.Step(`^file "([^"]*)" should exist$`, sc.fileShouldExist) ctx.Step(`^yaml file "([^"]*)" key "([^"]*)" should equal "([^"]*)"$`, sc.yamlFileKeyShouldEqual) ctx.Step(`^yaml file "([^"]*)" key "([^"]*)" should not be empty$`, sc.yamlFileKeyShouldNotBeEmpty) + ctx.Step(`^yaml file "([^"]*)" should have non-empty keys:$`, sc.yamlFileShouldHaveNonEmptyKeys) ctx.Step(`^yaml file "([^"]*)" should match:$`, sc.yamlFileShouldMatch) ctx.Step(`^yaml file "([^"]*)" key "([^"]*)" should match:$`, sc.yamlFileKeyShouldMatch) ctx.Step(`^yaml file "([^"]*)" should contain:$`, sc.yamlFileShouldContain) @@ -129,6 +130,14 @@ func (sc *ScenarioContext) yamlFileKeyShouldNotBeEmpty(path, key string) error { return nil } +func (sc *ScenarioContext) yamlFileShouldHaveNonEmptyKeys(path string, table *godog.Table) error { + keys, err := tableToSingleColumn(table, "key") + if err != nil { + return err + } + return dsl.RequireNonEmptyYAMLKeys(sc.resolvePath(dsl.Interpolate(path)), keys) +} + func (sc *ScenarioContext) yamlFileShouldMatch(path string, doc *godog.DocString) error { return dsl.MatchYAMLSubtree(sc.resolvePath(dsl.Interpolate(path)), "", doc.Content, dsl.MatchExact) } diff --git a/tests/bdd/steps/steps_test.go b/tests/bdd/steps/steps_test.go index 288ab1b8d..be05455c1 100644 --- a/tests/bdd/steps/steps_test.go +++ b/tests/bdd/steps/steps_test.go @@ -803,11 +803,62 @@ controlPlane: if err := sc.yamlFileKeyShouldNotBeEmpty(rel, "controlPlane.clusterName"); err != nil { t.Fatalf("not empty: %v", err) } + nonEmptyKeys := docTable(t, [][]string{ + {"key"}, + {"controlPlane.clusterName"}, + {"controlPlane.endpoints.inCluster.icmsURL"}, + }) + if err := sc.yamlFileShouldHaveNonEmptyKeys(rel, nonEmptyKeys); err != nil { + t.Fatalf("non-empty keys: %v", err) + } if err := sc.yamlFileKeyShouldContain(rel, "controlPlane.endpoints.inCluster", &godog.DocString{Content: "icmsURL: http://api.sis:8080\n"}); err != nil { t.Fatalf("contain: %v", err) } } +func TestYAMLFileShouldHaveNonEmptyKeysValidatesTable(t *testing.T) { + sc, _ := newScenarioContext(t) + + tests := []struct { + name string + table *godog.Table + want string + }{ + { + name: "no data rows", + table: docTable(t, [][]string{ + {"key"}, + }), + want: "at least one data row", + }, + { + name: "wrong header", + table: docTable(t, [][]string{ + {"name"}, + {"clusterID"}, + }), + want: `table header must be "key"`, + }, + { + name: "empty key", + table: docTable(t, [][]string{ + {"key"}, + {""}, + }), + want: "empty key value", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := sc.yamlFileShouldHaveNonEmptyKeys("registration.yaml", tc.table) + if err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("err = %v, want error containing %q", err, tc.want) + } + }) + } +} + func TestCommandOutputContainsAssertion(t *testing.T) { sc, _ := newScenarioContext(t) sc.LastResult = harness.Result{Stdout: "release deployed", Stderr: ""}