From 0d00c333f5e3e4c2c962e43ae2fcf9de0d38d754 Mon Sep 17 00:00:00 2001 From: Nandini Chandra Date: Wed, 1 Jul 2026 00:37:16 -0500 Subject: [PATCH 1/4] crane validate offline : Automate test for verifying malformed API surface file handling Signed-off-by: Nandini Chandra --- ...860_validate_malformed_api_surface_test.go | 363 ++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 e2e-tests/tests/tier1/mta_860_validate_malformed_api_surface_test.go diff --git a/e2e-tests/tests/tier1/mta_860_validate_malformed_api_surface_test.go b/e2e-tests/tests/tier1/mta_860_validate_malformed_api_surface_test.go new file mode 100644 index 00000000..04627f31 --- /dev/null +++ b/e2e-tests/tests/tier1/mta_860_validate_malformed_api_surface_test.go @@ -0,0 +1,363 @@ +package e2e + +import ( + "encoding/json" + "log" + "os" + "path/filepath" + + "github.com/konveyor/crane/e2e-tests/config" + . "github.com/konveyor/crane/e2e-tests/framework" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Crane validate offline mode: malformed API surface file handling", func() { + It("[MTA-860] Should handle malformed API surface JSON file gracefully as namespace admin", + Label("tier1", "validate", "offline"), func() { + appName := "multi-resource-app" + namespace := "validate-malformed-json" + + scenario := NewMigrationScenario( + appName, + namespace, + config.K8sDeployBin, + config.CraneBin, + config.SourceContext, + config.TargetContext, + ) + + if scenario.SrcAppNonAdmin.Context == "" { + Skip("source-nonadmin-context is required for non-admin offline validation test") + } + if scenario.TgtAppNonAdmin.Context == "" { + Skip("target-nonadmin-context is required for non-admin offline validation test") + } + + srcApp := scenario.SrcAppNonAdmin + tgtApp := scenario.TgtAppNonAdmin + runner := scenario.CraneNonAdmin + srcApp.ExtraVars = map[string]any{ + "non_admin_user": "true", + } + tgtApp.ExtraVars = srcApp.ExtraVars + + By("Grant ns admin permissions to nonadmin user on source and target") + kubectlSrcNonAdmin, _, cleanup, err := SetupNamespaceAdminUsersForScenario(scenario, namespace) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(func() { + By("Delete test namespace on source and target (wait for completion)") + for _, k := range []KubectlRunner{scenario.KubectlSrc, scenario.KubectlTgt} { + if _, err := k.Run("delete", "namespace", namespace, "--ignore-not-found=true", "--wait=true"); err != nil { + log.Printf("cleanup: failed to delete namespace %q on context %q: %v", namespace, k.Context, err) + } + } + }) + DeferCleanup(cleanup) // Cleanup rolebindings + + By("Prepare source app") + log.Printf("Preparing source app %s in namespace %s\n", srcApp.Name, srcApp.Namespace) + Expect(PrepareSourceApp(srcApp, kubectlSrcNonAdmin)).NotTo(HaveOccurred()) + log.Printf("Source app %s prepared successfully\n", srcApp.Name) + + paths, err := NewScenarioPaths("crane-validate-malformed-json-*") + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(func() { + By("Cleanup source and target resources") + if err := CleanupScenario(paths.TempDir, srcApp, tgtApp); err != nil { + log.Printf("cleanup: %v", err) + } + }) + + runner.WorkDir = paths.TempDir + By("Run crane export/transform/apply pipeline") + log.Printf("Running crane pipeline for namespace %s\n", srcApp.Namespace) + + exportOpts := ExportOptions{ + Namespace: srcApp.Namespace, + ExportDir: paths.ExportDir, + } + transformOpts := TransformOptions{ + ExportDir: paths.ExportDir, + TransformDir: paths.TransformDir, + } + applyOpts := ApplyOptions{ + ExportDir: paths.ExportDir, + TransformDir: paths.TransformDir, + OutputDir: paths.OutputDir, + } + + Expect(RunCranePipelineWithChecks(runner, exportOpts, transformOpts, applyOpts)).NotTo(HaveOccurred()) + log.Printf("Crane pipeline completed for namespace %s\n", srcApp.Namespace) + + By("Test Case 1: Invalid JSON syntax") + malformedFile := filepath.Join(paths.TempDir, "malformed-syntax.json") + malformedContent := `{ + "resources": [ + {"apiVersion": "v1", "kind": "Pod" + ] + }` // Missing closing brace for Pod object + Expect(os.WriteFile(malformedFile, []byte(malformedContent), 0644)).To(Succeed()) + + By("Run crane validate with malformed JSON file") + validateDir := filepath.Join(paths.TempDir, "validate-malformed-syntax") + stdout, err := runner.Validate(ValidateOptions{ + InputDir: filepath.Join(paths.OutputDir, "resources", namespace), + ValidateDir: validateDir, + APIResourcesFile: malformedFile, + }) + + By("Verify that crane validate returns an error") + Expect(err).To(HaveOccurred(), "crane validate should fail with malformed JSON") + log.Printf("Validate output: %s", stdout) + log.Printf("Validate error: %v", err) + + By("Verify error message indicates JSON parsing issue") + errMsg := err.Error() + Expect(errMsg).To(Or( + ContainSubstring("json"), + ContainSubstring("JSON"), + ContainSubstring("parse"), + ContainSubstring("unmarshal"), + ContainSubstring("invalid"), + ContainSubstring("syntax"), + ), "error message should indicate JSON parsing issue") + + By("Verify validation report was not created for malformed JSON") + reportPath := filepath.Join(validateDir, "report.json") + Expect(reportPath).NotTo(BeAnExistingFile(), "report.json should not be created with malformed JSON") + + log.Printf("✅ Test Case 1: Successfully validated error handling for malformed JSON syntax") + + By("Test Case 2: Empty JSON file") + emptyFile := filepath.Join(paths.TempDir, "empty-api-surface.json") + Expect(os.WriteFile(emptyFile, []byte(""), 0644)).To(Succeed()) + + By("Run crane validate with empty JSON file") + validateDir2 := filepath.Join(paths.TempDir, "validate-empty-json") + stdout2, err := runner.Validate(ValidateOptions{ + InputDir: filepath.Join(paths.OutputDir, "resources", namespace), + ValidateDir: validateDir2, + APIResourcesFile: emptyFile, + }) + + By("Verify that crane validate returns an error") + Expect(err).To(HaveOccurred(), "crane validate should fail with empty JSON file") + log.Printf("Validate output: %s", stdout2) + log.Printf("Validate error: %v", err) + + By("Verify error message indicates JSON parsing or empty file issue") + errMsg2 := err.Error() + Expect(errMsg2).To(Or( + ContainSubstring("json"), + ContainSubstring("JSON"), + ContainSubstring("parse"), + ContainSubstring("unmarshal"), + ContainSubstring("empty"), + ContainSubstring("EOF"), + ), "error message should indicate JSON parsing or empty file issue") + + log.Printf("✅ Test Case 2: Successfully validated error handling for empty JSON file") + + By("Test Case 3: Valid JSON but incorrect structure") + wrongStructureFile := filepath.Join(paths.TempDir, "wrong-structure.json") + wrongStructure := map[string]interface{}{ + "wrong_field": "value", + "resources": "should_be_array_not_string", + } + wrongJSON, err := json.Marshal(wrongStructure) + Expect(err).NotTo(HaveOccurred()) + Expect(os.WriteFile(wrongStructureFile, wrongJSON, 0644)).To(Succeed()) + + By("Run crane validate with wrong structure JSON file") + validateDir3 := filepath.Join(paths.TempDir, "validate-wrong-structure") + stdout3, err := runner.Validate(ValidateOptions{ + InputDir: filepath.Join(paths.OutputDir, "resources", namespace), + ValidateDir: validateDir3, + APIResourcesFile: wrongStructureFile, + }) + + if err != nil { + log.Printf("Validate returned error (expected): %v", err) + log.Printf("Validate output: %s", stdout3) + } else { + log.Printf("Validate succeeded (checking report content)") + // If it doesn't error, check if the report exists + reportPath3 := filepath.Join(validateDir3, "report.json") + if _, statErr := os.Stat(reportPath3); statErr == nil { + log.Printf("Report was created despite wrong structure") + } + } + + log.Printf("✅ Test Case 3: Validated behavior with wrong JSON structure") + + By("Test Case 4: Non-JSON content") + nonJSONFile := filepath.Join(paths.TempDir, "non-json.json") + nonJSONContent := "This is plain text, not JSON" + Expect(os.WriteFile(nonJSONFile, []byte(nonJSONContent), 0644)).To(Succeed()) + + By("Run crane validate with non-JSON file") + validateDir4 := filepath.Join(paths.TempDir, "validate-non-json") + stdout4, err := runner.Validate(ValidateOptions{ + InputDir: filepath.Join(paths.OutputDir, "resources", namespace), + ValidateDir: validateDir4, + APIResourcesFile: nonJSONFile, + }) + + By("Verify that crane validate returns an error") + Expect(err).To(HaveOccurred(), "crane validate should fail with non-JSON content") + log.Printf("Validate output: %s", stdout4) + log.Printf("Validate error: %v", err) + + By("Verify error message indicates JSON parsing issue") + errMsg4 := err.Error() + Expect(errMsg4).To(Or( + ContainSubstring("json"), + ContainSubstring("JSON"), + ContainSubstring("parse"), + ContainSubstring("unmarshal"), + ContainSubstring("invalid"), + ), "error message should indicate JSON parsing issue") + + log.Printf("✅ Test Case 4: Successfully validated error handling for non-JSON content") + + By("Test Case 5: Truncated/incomplete JSON") + By("Create truncated JSON file (incomplete)") + truncatedFile := filepath.Join(paths.TempDir, "truncated.json") + truncatedContent := `{ + "resources": [ + {"apiVersion": "v1", "kind": "Pod", "name": "test"` + // Missing closing brackets - simulates interrupted write + Expect(os.WriteFile(truncatedFile, []byte(truncatedContent), 0644)).To(Succeed()) + + By("Run crane validate with truncated JSON file") + validateDir5 := filepath.Join(paths.TempDir, "validate-truncated") + stdout5, err := runner.Validate(ValidateOptions{ + InputDir: filepath.Join(paths.OutputDir, "resources", namespace), + ValidateDir: validateDir5, + APIResourcesFile: truncatedFile, + }) + + By("Verify that crane validate returns an error") + Expect(err).To(HaveOccurred(), "crane validate should fail with truncated JSON") + log.Printf("Validate output: %s", stdout5) + log.Printf("Validate error: %v", err) + + By("Verify error message indicates JSON parsing issue") + errMsg5 := err.Error() + Expect(errMsg5).To(Or( + ContainSubstring("json"), + ContainSubstring("JSON"), + ContainSubstring("parse"), + ContainSubstring("unmarshal"), + ContainSubstring("unexpected"), + ContainSubstring("EOF"), + ), "error message should indicate JSON parsing issue") + + log.Printf("✅ Test Case 5: Successfully validated error handling for truncated JSON") + + By("Test Case 6: Array at root instead of object") + arrayRootFile := filepath.Join(paths.TempDir, "array-root.json") + arrayRootContent := `[ + {"apiVersion": "v1", "kind": "Pod"}, + {"apiVersion": "apps/v1", "kind": "Deployment"} + ]` + Expect(os.WriteFile(arrayRootFile, []byte(arrayRootContent), 0644)).To(Succeed()) + + By("Run crane validate with array-root JSON file") + validateDir6 := filepath.Join(paths.TempDir, "validate-array-root") + stdout6, err := runner.Validate(ValidateOptions{ + InputDir: filepath.Join(paths.OutputDir, "resources", namespace), + ValidateDir: validateDir6, + APIResourcesFile: arrayRootFile, + }) + + if err != nil { + log.Printf("Validate returned error: %v", err) + log.Printf("Validate output: %s", stdout6) + By("Verify error indicates type/structure issue") + errMsg6 := err.Error() + Expect(errMsg6).To(Or( + ContainSubstring("json"), + ContainSubstring("JSON"), + ContainSubstring("unmarshal"), + ContainSubstring("type"), + ContainSubstring("object"), + ), "error should indicate structure/type issue") + } else { + log.Printf("Validate succeeded - checking behavior") + } + + log.Printf("✅ Test Case 6: Validated behavior with array at root") + + By("Test Case 7: Mixed valid and invalid entries") + mixedFile := filepath.Join(paths.TempDir, "mixed-entries.json") + mixedContent := `{ + "resources": [ + {"apiVersion": "v1", "kind": "Pod"}, + {"apiVersion": "broken, "kind": "Deployment"}, + {"apiVersion": "v1", "kind": "Service"} + ] + }` + Expect(os.WriteFile(mixedFile, []byte(mixedContent), 0644)).To(Succeed()) + + By("Run crane validate with mixed entries") + validateDir7 := filepath.Join(paths.TempDir, "validate-mixed") + stdout7, err := runner.Validate(ValidateOptions{ + InputDir: filepath.Join(paths.OutputDir, "resources", namespace), + ValidateDir: validateDir7, + APIResourcesFile: mixedFile, + }) + + By("Verify that crane validate returns an error") + Expect(err).To(HaveOccurred(), "crane validate should fail with invalid JSON syntax") + log.Printf("Validate output: %s", stdout7) + log.Printf("Validate error: %v", err) + + By("Verify error message indicates JSON parsing issue") + errMsg7 := err.Error() + Expect(errMsg7).To(Or( + ContainSubstring("json"), + ContainSubstring("JSON"), + ContainSubstring("parse"), + ContainSubstring("unmarshal"), + ContainSubstring("syntax"), + ), "error message should indicate JSON parsing issue") + + log.Printf("✅ Test Case 7: Successfully validated error handling for mixed valid/invalid entries") + + By("Test Case 8: Binary data simulated with non-UTF8 bytes)") + binaryFile := filepath.Join(paths.TempDir, "binary.json") + // Create binary content - mix of valid JSON start with binary garbage + binaryContent := []byte{0x7B, 0x22, 0x72, 0x65, 0xFF, 0xFE, 0x00, 0x01, 0x80, 0x90} + Expect(os.WriteFile(binaryFile, binaryContent, 0644)).To(Succeed()) + + By("Run crane validate with binary file") + validateDir8 := filepath.Join(paths.TempDir, "validate-binary") + stdout8, err := runner.Validate(ValidateOptions{ + InputDir: filepath.Join(paths.OutputDir, "resources", namespace), + ValidateDir: validateDir8, + APIResourcesFile: binaryFile, + }) + + By("Verify that crane validate returns an error") + Expect(err).To(HaveOccurred(), "crane validate should fail with binary content") + log.Printf("Validate output: %s", stdout8) + log.Printf("Validate error: %v", err) + + By("Verify error indicates parsing or encoding issue") + errMsg8 := err.Error() + Expect(errMsg8).To(Or( + ContainSubstring("json"), + ContainSubstring("JSON"), + ContainSubstring("parse"), + ContainSubstring("unmarshal"), + ContainSubstring("invalid"), + ContainSubstring("character"), + ), "error message should indicate parsing or encoding issue") + + log.Printf("✅ Test Case 8: Successfully validated error handling for binary content") + + log.Printf("✅ MTA-860: All malformed API surface file scenarios validated successfully") + }) +}) From d46b851fd94987ba4532b77ff372c9c21c95af08 Mon Sep 17 00:00:00 2001 From: Nandini Chandra Date: Wed, 1 Jul 2026 15:55:57 -0500 Subject: [PATCH 2/4] crane validate offline : Automate test for verifying malformed API surface file handling Signed-off-by: Nandini Chandra --- ...860_validate_malformed_api_surface_test.go | 390 ++++++------------ 1 file changed, 129 insertions(+), 261 deletions(-) diff --git a/e2e-tests/tests/tier1/mta_860_validate_malformed_api_surface_test.go b/e2e-tests/tests/tier1/mta_860_validate_malformed_api_surface_test.go index 04627f31..7b6b323f 100644 --- a/e2e-tests/tests/tier1/mta_860_validate_malformed_api_surface_test.go +++ b/e2e-tests/tests/tier1/mta_860_validate_malformed_api_surface_test.go @@ -10,6 +10,7 @@ import ( . "github.com/konveyor/crane/e2e-tests/framework" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" ) var _ = Describe("Crane validate offline mode: malformed API surface file handling", func() { @@ -27,13 +28,6 @@ var _ = Describe("Crane validate offline mode: malformed API surface file handli config.TargetContext, ) - if scenario.SrcAppNonAdmin.Context == "" { - Skip("source-nonadmin-context is required for non-admin offline validation test") - } - if scenario.TgtAppNonAdmin.Context == "" { - Skip("target-nonadmin-context is required for non-admin offline validation test") - } - srcApp := scenario.SrcAppNonAdmin tgtApp := scenario.TgtAppNonAdmin runner := scenario.CraneNonAdmin @@ -43,7 +37,7 @@ var _ = Describe("Crane validate offline mode: malformed API surface file handli tgtApp.ExtraVars = srcApp.ExtraVars By("Grant ns admin permissions to nonadmin user on source and target") - kubectlSrcNonAdmin, _, cleanup, err := SetupNamespaceAdminUsersForScenario(scenario, namespace) + kubectlSrcNonAdmin, _, cleanup, err := SetupActiveKubectlRunners(scenario, namespace) Expect(err).NotTo(HaveOccurred()) DeferCleanup(func() { By("Delete test namespace on source and target (wait for completion)") @@ -90,273 +84,147 @@ var _ = Describe("Crane validate offline mode: malformed API surface file handli Expect(RunCranePipelineWithChecks(runner, exportOpts, transformOpts, applyOpts)).NotTo(HaveOccurred()) log.Printf("Crane pipeline completed for namespace %s\n", srcApp.Namespace) - By("Test Case 1: Invalid JSON syntax") - malformedFile := filepath.Join(paths.TempDir, "malformed-syntax.json") - malformedContent := `{ + // Define test cases for malformed JSON scenarios + type malformedJSONTestCase struct { + name string + fileContent interface{} // string for raw content, []byte for binary, or map for marshaled JSON + validateDirSuffix string + expectError bool + errorSubstrings []string + } + + testCases := []malformedJSONTestCase{ + { + name: "Invalid JSON syntax", + fileContent: `{ "resources": [ {"apiVersion": "v1", "kind": "Pod" ] - }` // Missing closing brace for Pod object - Expect(os.WriteFile(malformedFile, []byte(malformedContent), 0644)).To(Succeed()) - - By("Run crane validate with malformed JSON file") - validateDir := filepath.Join(paths.TempDir, "validate-malformed-syntax") - stdout, err := runner.Validate(ValidateOptions{ - InputDir: filepath.Join(paths.OutputDir, "resources", namespace), - ValidateDir: validateDir, - APIResourcesFile: malformedFile, - }) - - By("Verify that crane validate returns an error") - Expect(err).To(HaveOccurred(), "crane validate should fail with malformed JSON") - log.Printf("Validate output: %s", stdout) - log.Printf("Validate error: %v", err) - - By("Verify error message indicates JSON parsing issue") - errMsg := err.Error() - Expect(errMsg).To(Or( - ContainSubstring("json"), - ContainSubstring("JSON"), - ContainSubstring("parse"), - ContainSubstring("unmarshal"), - ContainSubstring("invalid"), - ContainSubstring("syntax"), - ), "error message should indicate JSON parsing issue") - - By("Verify validation report was not created for malformed JSON") - reportPath := filepath.Join(validateDir, "report.json") - Expect(reportPath).NotTo(BeAnExistingFile(), "report.json should not be created with malformed JSON") - - log.Printf("✅ Test Case 1: Successfully validated error handling for malformed JSON syntax") - - By("Test Case 2: Empty JSON file") - emptyFile := filepath.Join(paths.TempDir, "empty-api-surface.json") - Expect(os.WriteFile(emptyFile, []byte(""), 0644)).To(Succeed()) - - By("Run crane validate with empty JSON file") - validateDir2 := filepath.Join(paths.TempDir, "validate-empty-json") - stdout2, err := runner.Validate(ValidateOptions{ - InputDir: filepath.Join(paths.OutputDir, "resources", namespace), - ValidateDir: validateDir2, - APIResourcesFile: emptyFile, - }) - - By("Verify that crane validate returns an error") - Expect(err).To(HaveOccurred(), "crane validate should fail with empty JSON file") - log.Printf("Validate output: %s", stdout2) - log.Printf("Validate error: %v", err) - - By("Verify error message indicates JSON parsing or empty file issue") - errMsg2 := err.Error() - Expect(errMsg2).To(Or( - ContainSubstring("json"), - ContainSubstring("JSON"), - ContainSubstring("parse"), - ContainSubstring("unmarshal"), - ContainSubstring("empty"), - ContainSubstring("EOF"), - ), "error message should indicate JSON parsing or empty file issue") - - log.Printf("✅ Test Case 2: Successfully validated error handling for empty JSON file") - - By("Test Case 3: Valid JSON but incorrect structure") - wrongStructureFile := filepath.Join(paths.TempDir, "wrong-structure.json") - wrongStructure := map[string]interface{}{ - "wrong_field": "value", - "resources": "should_be_array_not_string", - } - wrongJSON, err := json.Marshal(wrongStructure) - Expect(err).NotTo(HaveOccurred()) - Expect(os.WriteFile(wrongStructureFile, wrongJSON, 0644)).To(Succeed()) - - By("Run crane validate with wrong structure JSON file") - validateDir3 := filepath.Join(paths.TempDir, "validate-wrong-structure") - stdout3, err := runner.Validate(ValidateOptions{ - InputDir: filepath.Join(paths.OutputDir, "resources", namespace), - ValidateDir: validateDir3, - APIResourcesFile: wrongStructureFile, - }) - - if err != nil { - log.Printf("Validate returned error (expected): %v", err) - log.Printf("Validate output: %s", stdout3) - } else { - log.Printf("Validate succeeded (checking report content)") - // If it doesn't error, check if the report exists - reportPath3 := filepath.Join(validateDir3, "report.json") - if _, statErr := os.Stat(reportPath3); statErr == nil { - log.Printf("Report was created despite wrong structure") - } - } - - log.Printf("✅ Test Case 3: Validated behavior with wrong JSON structure") - - By("Test Case 4: Non-JSON content") - nonJSONFile := filepath.Join(paths.TempDir, "non-json.json") - nonJSONContent := "This is plain text, not JSON" - Expect(os.WriteFile(nonJSONFile, []byte(nonJSONContent), 0644)).To(Succeed()) - - By("Run crane validate with non-JSON file") - validateDir4 := filepath.Join(paths.TempDir, "validate-non-json") - stdout4, err := runner.Validate(ValidateOptions{ - InputDir: filepath.Join(paths.OutputDir, "resources", namespace), - ValidateDir: validateDir4, - APIResourcesFile: nonJSONFile, - }) - - By("Verify that crane validate returns an error") - Expect(err).To(HaveOccurred(), "crane validate should fail with non-JSON content") - log.Printf("Validate output: %s", stdout4) - log.Printf("Validate error: %v", err) - - By("Verify error message indicates JSON parsing issue") - errMsg4 := err.Error() - Expect(errMsg4).To(Or( - ContainSubstring("json"), - ContainSubstring("JSON"), - ContainSubstring("parse"), - ContainSubstring("unmarshal"), - ContainSubstring("invalid"), - ), "error message should indicate JSON parsing issue") - - log.Printf("✅ Test Case 4: Successfully validated error handling for non-JSON content") - - By("Test Case 5: Truncated/incomplete JSON") - By("Create truncated JSON file (incomplete)") - truncatedFile := filepath.Join(paths.TempDir, "truncated.json") - truncatedContent := `{ + }`, // Missing closing brace for Pod object + validateDirSuffix: "malformed-syntax", + expectError: true, + errorSubstrings: []string{"json", "JSON", "parse", "unmarshal", "invalid", "syntax"}, + }, + { + name: "Empty JSON file", + fileContent: "", + validateDirSuffix: "empty-json", + expectError: true, + errorSubstrings: []string{"json", "JSON", "parse", "unmarshal", "empty", "EOF"}, + }, + { + name: "Valid JSON but incorrect structure", + fileContent: map[string]interface{}{ + "wrong_field": "value", + "resources": "should_be_array_not_string", + }, + validateDirSuffix: "wrong-structure", + expectError: true, + errorSubstrings: []string{"api-resources", "contains no API resource lists"}, + }, + { + name: "Non-JSON content", + fileContent: "This is plain text, not JSON", + validateDirSuffix: "non-json", + expectError: true, + errorSubstrings: []string{"json", "JSON", "parse", "unmarshal", "invalid"}, + }, + { + name: "Truncated/incomplete JSON", + fileContent: `{ "resources": [ - {"apiVersion": "v1", "kind": "Pod", "name": "test"` - // Missing closing brackets - simulates interrupted write - Expect(os.WriteFile(truncatedFile, []byte(truncatedContent), 0644)).To(Succeed()) - - By("Run crane validate with truncated JSON file") - validateDir5 := filepath.Join(paths.TempDir, "validate-truncated") - stdout5, err := runner.Validate(ValidateOptions{ - InputDir: filepath.Join(paths.OutputDir, "resources", namespace), - ValidateDir: validateDir5, - APIResourcesFile: truncatedFile, - }) - - By("Verify that crane validate returns an error") - Expect(err).To(HaveOccurred(), "crane validate should fail with truncated JSON") - log.Printf("Validate output: %s", stdout5) - log.Printf("Validate error: %v", err) - - By("Verify error message indicates JSON parsing issue") - errMsg5 := err.Error() - Expect(errMsg5).To(Or( - ContainSubstring("json"), - ContainSubstring("JSON"), - ContainSubstring("parse"), - ContainSubstring("unmarshal"), - ContainSubstring("unexpected"), - ContainSubstring("EOF"), - ), "error message should indicate JSON parsing issue") - - log.Printf("✅ Test Case 5: Successfully validated error handling for truncated JSON") - - By("Test Case 6: Array at root instead of object") - arrayRootFile := filepath.Join(paths.TempDir, "array-root.json") - arrayRootContent := `[ + {"apiVersion": "v1", "kind": "Pod", "name": "test"`, + validateDirSuffix: "truncated", + expectError: true, + errorSubstrings: []string{"json", "JSON", "parse", "unmarshal", "unexpected", "EOF"}, + }, + { + name: "Array at root instead of object", + fileContent: `[ {"apiVersion": "v1", "kind": "Pod"}, {"apiVersion": "apps/v1", "kind": "Deployment"} - ]` - Expect(os.WriteFile(arrayRootFile, []byte(arrayRootContent), 0644)).To(Succeed()) - - By("Run crane validate with array-root JSON file") - validateDir6 := filepath.Join(paths.TempDir, "validate-array-root") - stdout6, err := runner.Validate(ValidateOptions{ - InputDir: filepath.Join(paths.OutputDir, "resources", namespace), - ValidateDir: validateDir6, - APIResourcesFile: arrayRootFile, - }) - - if err != nil { - log.Printf("Validate returned error: %v", err) - log.Printf("Validate output: %s", stdout6) - By("Verify error indicates type/structure issue") - errMsg6 := err.Error() - Expect(errMsg6).To(Or( - ContainSubstring("json"), - ContainSubstring("JSON"), - ContainSubstring("unmarshal"), - ContainSubstring("type"), - ContainSubstring("object"), - ), "error should indicate structure/type issue") - } else { - log.Printf("Validate succeeded - checking behavior") - } - - log.Printf("✅ Test Case 6: Validated behavior with array at root") - - By("Test Case 7: Mixed valid and invalid entries") - mixedFile := filepath.Join(paths.TempDir, "mixed-entries.json") - mixedContent := `{ + ]`, + validateDirSuffix: "array-root", + expectError: true, + errorSubstrings: []string{"json", "JSON", "unmarshal", "type", "cannot"}, + }, + { + name: "Mixed valid and invalid entries", + fileContent: `{ "resources": [ {"apiVersion": "v1", "kind": "Pod"}, {"apiVersion": "broken, "kind": "Deployment"}, {"apiVersion": "v1", "kind": "Service"} ] - }` - Expect(os.WriteFile(mixedFile, []byte(mixedContent), 0644)).To(Succeed()) - - By("Run crane validate with mixed entries") - validateDir7 := filepath.Join(paths.TempDir, "validate-mixed") - stdout7, err := runner.Validate(ValidateOptions{ - InputDir: filepath.Join(paths.OutputDir, "resources", namespace), - ValidateDir: validateDir7, - APIResourcesFile: mixedFile, - }) - - By("Verify that crane validate returns an error") - Expect(err).To(HaveOccurred(), "crane validate should fail with invalid JSON syntax") - log.Printf("Validate output: %s", stdout7) - log.Printf("Validate error: %v", err) - - By("Verify error message indicates JSON parsing issue") - errMsg7 := err.Error() - Expect(errMsg7).To(Or( - ContainSubstring("json"), - ContainSubstring("JSON"), - ContainSubstring("parse"), - ContainSubstring("unmarshal"), - ContainSubstring("syntax"), - ), "error message should indicate JSON parsing issue") - - log.Printf("✅ Test Case 7: Successfully validated error handling for mixed valid/invalid entries") - - By("Test Case 8: Binary data simulated with non-UTF8 bytes)") - binaryFile := filepath.Join(paths.TempDir, "binary.json") - // Create binary content - mix of valid JSON start with binary garbage - binaryContent := []byte{0x7B, 0x22, 0x72, 0x65, 0xFF, 0xFE, 0x00, 0x01, 0x80, 0x90} - Expect(os.WriteFile(binaryFile, binaryContent, 0644)).To(Succeed()) - - By("Run crane validate with binary file") - validateDir8 := filepath.Join(paths.TempDir, "validate-binary") - stdout8, err := runner.Validate(ValidateOptions{ - InputDir: filepath.Join(paths.OutputDir, "resources", namespace), - ValidateDir: validateDir8, - APIResourcesFile: binaryFile, - }) + }`, + validateDirSuffix: "mixed", + expectError: true, + errorSubstrings: []string{"json", "JSON", "parse", "unmarshal", "syntax"}, + }, + { + name: "Binary data with non-UTF8 bytes", + fileContent: []byte{0x7B, 0x22, 0x72, 0x65, 0xFF, 0xFE, 0x00, 0x01, 0x80, 0x90}, + validateDirSuffix: "binary", + expectError: true, + errorSubstrings: []string{"json", "JSON", "parse", "unmarshal", "invalid", "character"}, + }, + } - By("Verify that crane validate returns an error") - Expect(err).To(HaveOccurred(), "crane validate should fail with binary content") - log.Printf("Validate output: %s", stdout8) - log.Printf("Validate error: %v", err) + // Execute test cases in a loop + for i, tc := range testCases { + testNum := i + 1 + By("Test Case " + tc.name) + + // Create test file with appropriate content + testFile := filepath.Join(paths.TempDir, tc.validateDirSuffix+".json") + switch content := tc.fileContent.(type) { + case string: + Expect(os.WriteFile(testFile, []byte(content), 0644)).To(Succeed()) + case []byte: + Expect(os.WriteFile(testFile, content, 0644)).To(Succeed()) + case map[string]interface{}: + jsonBytes, err := json.Marshal(content) + Expect(err).NotTo(HaveOccurred()) + Expect(os.WriteFile(testFile, jsonBytes, 0644)).To(Succeed()) + } - By("Verify error indicates parsing or encoding issue") - errMsg8 := err.Error() - Expect(errMsg8).To(Or( - ContainSubstring("json"), - ContainSubstring("JSON"), - ContainSubstring("parse"), - ContainSubstring("unmarshal"), - ContainSubstring("invalid"), - ContainSubstring("character"), - ), "error message should indicate parsing or encoding issue") + // Run crane validate + validateDir := filepath.Join(paths.TempDir, "validate-"+tc.validateDirSuffix) + stdout, err := runner.Validate(ValidateOptions{ + InputDir: filepath.Join(paths.OutputDir, "resources", namespace), + ValidateDir: validateDir, + APIResourcesFile: testFile, + }) + + // Verify error expectation + if tc.expectError { + Expect(err).To(HaveOccurred(), "crane validate should fail with "+tc.name) + log.Printf("Validate output: %s", stdout) + log.Printf("Validate error: %v", err) + + // Verify error message contains expected substrings + errMsg := err.Error() + matchers := make([]types.GomegaMatcher, len(tc.errorSubstrings)) + for i, substr := range tc.errorSubstrings { + matchers[i] = ContainSubstring(substr) + } + Expect(errMsg).To(Or(matchers...), "error message should indicate expected issue") + + // Verify validation report was not created + reportPath := filepath.Join(validateDir, "report.json") + Expect(reportPath).NotTo(BeAnExistingFile(), "report.json should not be created with malformed JSON") + } else { + // For test cases expecting successful validation + Expect(err).NotTo(HaveOccurred(), "crane validate should succeed with "+tc.name) + log.Printf("Validate output: %s", stdout) + + // Verify validation report was created successfully + reportPath := filepath.Join(validateDir, "report.json") + Expect(reportPath).To(BeAnExistingFile(), "report.json should be created for successful validation") + } - log.Printf("✅ Test Case 8: Successfully validated error handling for binary content") + log.Printf("✅ Test Case %d: Successfully validated error handling for %s", testNum, tc.name) + } log.Printf("✅ MTA-860: All malformed API surface file scenarios validated successfully") }) From 7339fa6f7cf45f3e6ebaf57c1de6b07dc001537a Mon Sep 17 00:00:00 2001 From: Nandini Chandra Date: Wed, 1 Jul 2026 21:24:07 -0500 Subject: [PATCH 3/4] crane validate offline : Automate test for verifying malformed API surface file handling Signed-off-by: Nandini Chandra --- ...860_validate_malformed_api_surface_test.go | 137 ++++++++++-------- 1 file changed, 79 insertions(+), 58 deletions(-) diff --git a/e2e-tests/tests/tier1/mta_860_validate_malformed_api_surface_test.go b/e2e-tests/tests/tier1/mta_860_validate_malformed_api_surface_test.go index 7b6b323f..998dc879 100644 --- a/e2e-tests/tests/tier1/mta_860_validate_malformed_api_surface_test.go +++ b/e2e-tests/tests/tier1/mta_860_validate_malformed_api_surface_test.go @@ -2,6 +2,8 @@ package e2e import ( "encoding/json" + "errors" + "fmt" "log" "os" "path/filepath" @@ -34,7 +36,9 @@ var _ = Describe("Crane validate offline mode: malformed API surface file handli srcApp.ExtraVars = map[string]any{ "non_admin_user": "true", } - tgtApp.ExtraVars = srcApp.ExtraVars + tgtApp.ExtraVars = map[string]any{ + "non_admin_user": "true", + } By("Grant ns admin permissions to nonadmin user on source and target") kubectlSrcNonAdmin, _, cleanup, err := SetupActiveKubectlRunners(scenario, namespace) @@ -103,14 +107,14 @@ var _ = Describe("Crane validate offline mode: malformed API surface file handli }`, // Missing closing brace for Pod object validateDirSuffix: "malformed-syntax", expectError: true, - errorSubstrings: []string{"json", "JSON", "parse", "unmarshal", "invalid", "syntax"}, + errorSubstrings: []string{"invalid character"}, }, { name: "Empty JSON file", fileContent: "", validateDirSuffix: "empty-json", expectError: true, - errorSubstrings: []string{"json", "JSON", "parse", "unmarshal", "empty", "EOF"}, + errorSubstrings: []string{"unexpected end of JSON input"}, }, { name: "Valid JSON but incorrect structure", @@ -120,14 +124,14 @@ var _ = Describe("Crane validate offline mode: malformed API surface file handli }, validateDirSuffix: "wrong-structure", expectError: true, - errorSubstrings: []string{"api-resources", "contains no API resource lists"}, + errorSubstrings: []string{"contains no API resource lists"}, }, { name: "Non-JSON content", fileContent: "This is plain text, not JSON", validateDirSuffix: "non-json", expectError: true, - errorSubstrings: []string{"json", "JSON", "parse", "unmarshal", "invalid"}, + errorSubstrings: []string{"invalid character"}, }, { name: "Truncated/incomplete JSON", @@ -136,7 +140,7 @@ var _ = Describe("Crane validate offline mode: malformed API surface file handli {"apiVersion": "v1", "kind": "Pod", "name": "test"`, validateDirSuffix: "truncated", expectError: true, - errorSubstrings: []string{"json", "JSON", "parse", "unmarshal", "unexpected", "EOF"}, + errorSubstrings: []string{"unexpected end of JSON input"}, }, { name: "Array at root instead of object", @@ -146,7 +150,7 @@ var _ = Describe("Crane validate offline mode: malformed API surface file handli ]`, validateDirSuffix: "array-root", expectError: true, - errorSubstrings: []string{"json", "JSON", "unmarshal", "type", "cannot"}, + errorSubstrings: []string{"cannot unmarshal array"}, }, { name: "Mixed valid and invalid entries", @@ -159,73 +163,90 @@ var _ = Describe("Crane validate offline mode: malformed API surface file handli }`, validateDirSuffix: "mixed", expectError: true, - errorSubstrings: []string{"json", "JSON", "parse", "unmarshal", "syntax"}, + errorSubstrings: []string{"invalid character"}, }, { name: "Binary data with non-UTF8 bytes", fileContent: []byte{0x7B, 0x22, 0x72, 0x65, 0xFF, 0xFE, 0x00, 0x01, 0x80, 0x90}, validateDirSuffix: "binary", expectError: true, - errorSubstrings: []string{"json", "JSON", "parse", "unmarshal", "invalid", "character"}, + errorSubstrings: []string{"invalid character"}, }, } // Execute test cases in a loop for i, tc := range testCases { testNum := i + 1 - By("Test Case " + tc.name) - - // Create test file with appropriate content - testFile := filepath.Join(paths.TempDir, tc.validateDirSuffix+".json") - switch content := tc.fileContent.(type) { - case string: - Expect(os.WriteFile(testFile, []byte(content), 0644)).To(Succeed()) - case []byte: - Expect(os.WriteFile(testFile, content, 0644)).To(Succeed()) - case map[string]interface{}: - jsonBytes, err := json.Marshal(content) - Expect(err).NotTo(HaveOccurred()) - Expect(os.WriteFile(testFile, jsonBytes, 0644)).To(Succeed()) - } + log.Printf("\n========================================") + By(fmt.Sprintf("▶️ Test Case %d: %s", testNum, tc.name)) + + // Wrap test case in a function to recover from panics and continue with remaining cases + func() { + defer GinkgoRecover() + + // Create test file with appropriate content + testFile := filepath.Join(paths.TempDir, tc.validateDirSuffix+".json") + switch content := tc.fileContent.(type) { + case string: + Expect(os.WriteFile(testFile, []byte(content), 0644)).To(Succeed()) + case []byte: + Expect(os.WriteFile(testFile, content, 0644)).To(Succeed()) + case map[string]interface{}: + jsonBytes, err := json.Marshal(content) + Expect(err).NotTo(HaveOccurred()) + Expect(os.WriteFile(testFile, jsonBytes, 0644)).To(Succeed()) + default: + Fail(fmt.Sprintf("unsupported fileContent type %T for test case %q", content, tc.name)) + } - // Run crane validate - validateDir := filepath.Join(paths.TempDir, "validate-"+tc.validateDirSuffix) - stdout, err := runner.Validate(ValidateOptions{ - InputDir: filepath.Join(paths.OutputDir, "resources", namespace), - ValidateDir: validateDir, - APIResourcesFile: testFile, - }) - - // Verify error expectation - if tc.expectError { - Expect(err).To(HaveOccurred(), "crane validate should fail with "+tc.name) - log.Printf("Validate output: %s", stdout) - log.Printf("Validate error: %v", err) - - // Verify error message contains expected substrings - errMsg := err.Error() - matchers := make([]types.GomegaMatcher, len(tc.errorSubstrings)) - for i, substr := range tc.errorSubstrings { - matchers[i] = ContainSubstring(substr) + // Run crane validate + validateDir := filepath.Join(paths.TempDir, "validate-"+tc.validateDirSuffix) + stdout, err := runner.Validate(ValidateOptions{ + InputDir: filepath.Join(paths.OutputDir, "resources", namespace), + ValidateDir: validateDir, + APIResourcesFile: testFile, + }) + + // Verify error expectation + if tc.expectError { + Expect(err).To(HaveOccurred(), "crane validate should fail with "+tc.name) + log.Printf("Validate output: %s", stdout) + log.Printf("Validate error: %v", err) + + // Verify error message contains expected substrings + // Unwrap to get the root cause error, not just the wrapper + rootErr := err + for { + unwrapped := errors.Unwrap(rootErr) + if unwrapped == nil { + break + } + rootErr = unwrapped + } + rootErrMsg := rootErr.Error() + matchers := make([]types.GomegaMatcher, len(tc.errorSubstrings)) + for idx, substr := range tc.errorSubstrings { + matchers[idx] = ContainSubstring(substr) + } + Expect(rootErrMsg).To(Or(matchers...), "root error message should indicate expected issue") + + // Verify validation report was not created + reportPath := filepath.Join(validateDir, "report.json") + Expect(reportPath).NotTo(BeAnExistingFile(), "report.json should not be created with malformed JSON") + } else { + // For test cases expecting successful validation + Expect(err).NotTo(HaveOccurred(), "crane validate should succeed with "+tc.name) + log.Printf("Validate output: %s", stdout) + + // Verify validation report was created successfully + reportPath := filepath.Join(validateDir, "report.json") + Expect(reportPath).To(BeAnExistingFile(), "report.json should be created for successful validation") } - Expect(errMsg).To(Or(matchers...), "error message should indicate expected issue") - - // Verify validation report was not created - reportPath := filepath.Join(validateDir, "report.json") - Expect(reportPath).NotTo(BeAnExistingFile(), "report.json should not be created with malformed JSON") - } else { - // For test cases expecting successful validation - Expect(err).NotTo(HaveOccurred(), "crane validate should succeed with "+tc.name) - log.Printf("Validate output: %s", stdout) - - // Verify validation report was created successfully - reportPath := filepath.Join(validateDir, "report.json") - Expect(reportPath).To(BeAnExistingFile(), "report.json should be created for successful validation") - } - log.Printf("✅ Test Case %d: Successfully validated error handling for %s", testNum, tc.name) + log.Printf("✅ Test Case %d: Successfully validated error handling for %s", testNum, tc.name) + }() } - + log.Printf("\n========================================") log.Printf("✅ MTA-860: All malformed API surface file scenarios validated successfully") }) }) From 6b625306a6073099e9bcf110c51f42a91ffc1421 Mon Sep 17 00:00:00 2001 From: Nandini Chandra Date: Thu, 2 Jul 2026 08:41:26 -0500 Subject: [PATCH 4/4] crane validate offline : Automate test for verifying malformed API surface file handling Signed-off-by: Nandini Chandra --- ...860_validate_malformed_api_surface_test.go | 67 +++++++------------ 1 file changed, 24 insertions(+), 43 deletions(-) diff --git a/e2e-tests/tests/tier1/mta_860_validate_malformed_api_surface_test.go b/e2e-tests/tests/tier1/mta_860_validate_malformed_api_surface_test.go index 998dc879..353f5c39 100644 --- a/e2e-tests/tests/tier1/mta_860_validate_malformed_api_surface_test.go +++ b/e2e-tests/tests/tier1/mta_860_validate_malformed_api_surface_test.go @@ -93,8 +93,7 @@ var _ = Describe("Crane validate offline mode: malformed API surface file handli name string fileContent interface{} // string for raw content, []byte for binary, or map for marshaled JSON validateDirSuffix string - expectError bool - errorSubstrings []string + errorSubstrings []string // Expected substrings in the root error message } testCases := []malformedJSONTestCase{ @@ -106,14 +105,12 @@ var _ = Describe("Crane validate offline mode: malformed API surface file handli ] }`, // Missing closing brace for Pod object validateDirSuffix: "malformed-syntax", - expectError: true, errorSubstrings: []string{"invalid character"}, }, { name: "Empty JSON file", fileContent: "", validateDirSuffix: "empty-json", - expectError: true, errorSubstrings: []string{"unexpected end of JSON input"}, }, { @@ -123,14 +120,12 @@ var _ = Describe("Crane validate offline mode: malformed API surface file handli "resources": "should_be_array_not_string", }, validateDirSuffix: "wrong-structure", - expectError: true, errorSubstrings: []string{"contains no API resource lists"}, }, { name: "Non-JSON content", fileContent: "This is plain text, not JSON", validateDirSuffix: "non-json", - expectError: true, errorSubstrings: []string{"invalid character"}, }, { @@ -139,7 +134,6 @@ var _ = Describe("Crane validate offline mode: malformed API surface file handli "resources": [ {"apiVersion": "v1", "kind": "Pod", "name": "test"`, validateDirSuffix: "truncated", - expectError: true, errorSubstrings: []string{"unexpected end of JSON input"}, }, { @@ -149,7 +143,6 @@ var _ = Describe("Crane validate offline mode: malformed API surface file handli {"apiVersion": "apps/v1", "kind": "Deployment"} ]`, validateDirSuffix: "array-root", - expectError: true, errorSubstrings: []string{"cannot unmarshal array"}, }, { @@ -162,14 +155,12 @@ var _ = Describe("Crane validate offline mode: malformed API surface file handli ] }`, validateDirSuffix: "mixed", - expectError: true, errorSubstrings: []string{"invalid character"}, }, { name: "Binary data with non-UTF8 bytes", fileContent: []byte{0x7B, 0x22, 0x72, 0x65, 0xFF, 0xFE, 0x00, 0x01, 0x80, 0x90}, validateDirSuffix: "binary", - expectError: true, errorSubstrings: []string{"invalid character"}, }, } @@ -207,41 +198,31 @@ var _ = Describe("Crane validate offline mode: malformed API surface file handli APIResourcesFile: testFile, }) - // Verify error expectation - if tc.expectError { - Expect(err).To(HaveOccurred(), "crane validate should fail with "+tc.name) - log.Printf("Validate output: %s", stdout) - log.Printf("Validate error: %v", err) - - // Verify error message contains expected substrings - // Unwrap to get the root cause error, not just the wrapper - rootErr := err - for { - unwrapped := errors.Unwrap(rootErr) - if unwrapped == nil { - break - } - rootErr = unwrapped + // All test cases expect crane validate to fail with malformed JSON + Expect(err).To(HaveOccurred(), "crane validate should fail with "+tc.name) + log.Printf("Validate output: %s", stdout) + log.Printf("Validate error: %v", err) + + // Verify error message contains expected substrings + // Unwrap to get the root cause error, not just the wrapper + rootErr := err + for { + unwrapped := errors.Unwrap(rootErr) + if unwrapped == nil { + break } - rootErrMsg := rootErr.Error() - matchers := make([]types.GomegaMatcher, len(tc.errorSubstrings)) - for idx, substr := range tc.errorSubstrings { - matchers[idx] = ContainSubstring(substr) - } - Expect(rootErrMsg).To(Or(matchers...), "root error message should indicate expected issue") - - // Verify validation report was not created - reportPath := filepath.Join(validateDir, "report.json") - Expect(reportPath).NotTo(BeAnExistingFile(), "report.json should not be created with malformed JSON") - } else { - // For test cases expecting successful validation - Expect(err).NotTo(HaveOccurred(), "crane validate should succeed with "+tc.name) - log.Printf("Validate output: %s", stdout) - - // Verify validation report was created successfully - reportPath := filepath.Join(validateDir, "report.json") - Expect(reportPath).To(BeAnExistingFile(), "report.json should be created for successful validation") + rootErr = unwrapped + } + rootErrMsg := rootErr.Error() + matchers := make([]types.GomegaMatcher, len(tc.errorSubstrings)) + for idx, substr := range tc.errorSubstrings { + matchers[idx] = ContainSubstring(substr) } + Expect(rootErrMsg).To(Or(matchers...), "root error message should indicate expected issue") + + // Verify validation report was not created + reportPath := filepath.Join(validateDir, "report.json") + Expect(reportPath).NotTo(BeAnExistingFile(), "report.json should not be created with malformed JSON") log.Printf("✅ Test Case %d: Successfully validated error handling for %s", testNum, tc.name) }()