Skip to content

Commit 12bbeea

Browse files
authored
🧪 crane validate offline mode : Automate test for malformed API surface file handling (#592)
* crane validate offline : Automate test for verifying malformed API surface file handling Signed-off-by: Nandini Chandra <nachandr@redhat.com> * crane validate offline : Automate test for verifying malformed API surface file handling Signed-off-by: Nandini Chandra <nachandr@redhat.com> * crane validate offline : Automate test for verifying malformed API surface file handling Signed-off-by: Nandini Chandra <nachandr@redhat.com> * crane validate offline : Automate test for verifying malformed API surface file handling Signed-off-by: Nandini Chandra <nachandr@redhat.com> --------- Signed-off-by: Nandini Chandra <nachandr@redhat.com>
1 parent 45826fe commit 12bbeea

1 file changed

Lines changed: 233 additions & 0 deletions

File tree

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
package e2e
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"log"
8+
"os"
9+
"path/filepath"
10+
11+
"github.com/konveyor/crane/e2e-tests/config"
12+
. "github.com/konveyor/crane/e2e-tests/framework"
13+
. "github.com/onsi/ginkgo/v2"
14+
. "github.com/onsi/gomega"
15+
"github.com/onsi/gomega/types"
16+
)
17+
18+
var _ = Describe("Crane validate offline mode: malformed API surface file handling", func() {
19+
It("[MTA-860] Should handle malformed API surface JSON file gracefully as namespace admin",
20+
Label("tier1", "validate", "offline"), func() {
21+
appName := "multi-resource-app"
22+
namespace := "validate-malformed-json"
23+
24+
scenario := NewMigrationScenario(
25+
appName,
26+
namespace,
27+
config.K8sDeployBin,
28+
config.CraneBin,
29+
config.SourceContext,
30+
config.TargetContext,
31+
)
32+
33+
srcApp := scenario.SrcAppNonAdmin
34+
tgtApp := scenario.TgtAppNonAdmin
35+
runner := scenario.CraneNonAdmin
36+
srcApp.ExtraVars = map[string]any{
37+
"non_admin_user": "true",
38+
}
39+
tgtApp.ExtraVars = map[string]any{
40+
"non_admin_user": "true",
41+
}
42+
43+
By("Grant ns admin permissions to nonadmin user on source and target")
44+
kubectlSrcNonAdmin, _, cleanup, err := SetupActiveKubectlRunners(scenario, namespace)
45+
Expect(err).NotTo(HaveOccurred())
46+
DeferCleanup(func() {
47+
By("Delete test namespace on source and target (wait for completion)")
48+
for _, k := range []KubectlRunner{scenario.KubectlSrc, scenario.KubectlTgt} {
49+
if _, err := k.Run("delete", "namespace", namespace, "--ignore-not-found=true", "--wait=true"); err != nil {
50+
log.Printf("cleanup: failed to delete namespace %q on context %q: %v", namespace, k.Context, err)
51+
}
52+
}
53+
})
54+
DeferCleanup(cleanup) // Cleanup rolebindings
55+
56+
By("Prepare source app")
57+
log.Printf("Preparing source app %s in namespace %s\n", srcApp.Name, srcApp.Namespace)
58+
Expect(PrepareSourceApp(srcApp, kubectlSrcNonAdmin)).NotTo(HaveOccurred())
59+
log.Printf("Source app %s prepared successfully\n", srcApp.Name)
60+
61+
paths, err := NewScenarioPaths("crane-validate-malformed-json-*")
62+
Expect(err).NotTo(HaveOccurred())
63+
DeferCleanup(func() {
64+
By("Cleanup source and target resources")
65+
if err := CleanupScenario(paths.TempDir, srcApp, tgtApp); err != nil {
66+
log.Printf("cleanup: %v", err)
67+
}
68+
})
69+
70+
runner.WorkDir = paths.TempDir
71+
By("Run crane export/transform/apply pipeline")
72+
log.Printf("Running crane pipeline for namespace %s\n", srcApp.Namespace)
73+
74+
exportOpts := ExportOptions{
75+
Namespace: srcApp.Namespace,
76+
ExportDir: paths.ExportDir,
77+
}
78+
transformOpts := TransformOptions{
79+
ExportDir: paths.ExportDir,
80+
TransformDir: paths.TransformDir,
81+
}
82+
applyOpts := ApplyOptions{
83+
ExportDir: paths.ExportDir,
84+
TransformDir: paths.TransformDir,
85+
OutputDir: paths.OutputDir,
86+
}
87+
88+
Expect(RunCranePipelineWithChecks(runner, exportOpts, transformOpts, applyOpts)).NotTo(HaveOccurred())
89+
log.Printf("Crane pipeline completed for namespace %s\n", srcApp.Namespace)
90+
91+
// Define test cases for malformed JSON scenarios
92+
type malformedJSONTestCase struct {
93+
name string
94+
fileContent interface{} // string for raw content, []byte for binary, or map for marshaled JSON
95+
validateDirSuffix string
96+
errorSubstrings []string // Expected substrings in the root error message
97+
}
98+
99+
testCases := []malformedJSONTestCase{
100+
{
101+
name: "Invalid JSON syntax",
102+
fileContent: `{
103+
"resources": [
104+
{"apiVersion": "v1", "kind": "Pod"
105+
]
106+
}`, // Missing closing brace for Pod object
107+
validateDirSuffix: "malformed-syntax",
108+
errorSubstrings: []string{"invalid character"},
109+
},
110+
{
111+
name: "Empty JSON file",
112+
fileContent: "",
113+
validateDirSuffix: "empty-json",
114+
errorSubstrings: []string{"unexpected end of JSON input"},
115+
},
116+
{
117+
name: "Valid JSON but incorrect structure",
118+
fileContent: map[string]interface{}{
119+
"wrong_field": "value",
120+
"resources": "should_be_array_not_string",
121+
},
122+
validateDirSuffix: "wrong-structure",
123+
errorSubstrings: []string{"contains no API resource lists"},
124+
},
125+
{
126+
name: "Non-JSON content",
127+
fileContent: "This is plain text, not JSON",
128+
validateDirSuffix: "non-json",
129+
errorSubstrings: []string{"invalid character"},
130+
},
131+
{
132+
name: "Truncated/incomplete JSON",
133+
fileContent: `{
134+
"resources": [
135+
{"apiVersion": "v1", "kind": "Pod", "name": "test"`,
136+
validateDirSuffix: "truncated",
137+
errorSubstrings: []string{"unexpected end of JSON input"},
138+
},
139+
{
140+
name: "Array at root instead of object",
141+
fileContent: `[
142+
{"apiVersion": "v1", "kind": "Pod"},
143+
{"apiVersion": "apps/v1", "kind": "Deployment"}
144+
]`,
145+
validateDirSuffix: "array-root",
146+
errorSubstrings: []string{"cannot unmarshal array"},
147+
},
148+
{
149+
name: "Mixed valid and invalid entries",
150+
fileContent: `{
151+
"resources": [
152+
{"apiVersion": "v1", "kind": "Pod"},
153+
{"apiVersion": "broken, "kind": "Deployment"},
154+
{"apiVersion": "v1", "kind": "Service"}
155+
]
156+
}`,
157+
validateDirSuffix: "mixed",
158+
errorSubstrings: []string{"invalid character"},
159+
},
160+
{
161+
name: "Binary data with non-UTF8 bytes",
162+
fileContent: []byte{0x7B, 0x22, 0x72, 0x65, 0xFF, 0xFE, 0x00, 0x01, 0x80, 0x90},
163+
validateDirSuffix: "binary",
164+
errorSubstrings: []string{"invalid character"},
165+
},
166+
}
167+
168+
// Execute test cases in a loop
169+
for i, tc := range testCases {
170+
testNum := i + 1
171+
log.Printf("\n========================================")
172+
By(fmt.Sprintf("▶️ Test Case %d: %s", testNum, tc.name))
173+
174+
// Wrap test case in a function to recover from panics and continue with remaining cases
175+
func() {
176+
defer GinkgoRecover()
177+
178+
// Create test file with appropriate content
179+
testFile := filepath.Join(paths.TempDir, tc.validateDirSuffix+".json")
180+
switch content := tc.fileContent.(type) {
181+
case string:
182+
Expect(os.WriteFile(testFile, []byte(content), 0644)).To(Succeed())
183+
case []byte:
184+
Expect(os.WriteFile(testFile, content, 0644)).To(Succeed())
185+
case map[string]interface{}:
186+
jsonBytes, err := json.Marshal(content)
187+
Expect(err).NotTo(HaveOccurred())
188+
Expect(os.WriteFile(testFile, jsonBytes, 0644)).To(Succeed())
189+
default:
190+
Fail(fmt.Sprintf("unsupported fileContent type %T for test case %q", content, tc.name))
191+
}
192+
193+
// Run crane validate
194+
validateDir := filepath.Join(paths.TempDir, "validate-"+tc.validateDirSuffix)
195+
stdout, err := runner.Validate(ValidateOptions{
196+
InputDir: filepath.Join(paths.OutputDir, "resources", namespace),
197+
ValidateDir: validateDir,
198+
APIResourcesFile: testFile,
199+
})
200+
201+
// All test cases expect crane validate to fail with malformed JSON
202+
Expect(err).To(HaveOccurred(), "crane validate should fail with "+tc.name)
203+
log.Printf("Validate output: %s", stdout)
204+
log.Printf("Validate error: %v", err)
205+
206+
// Verify error message contains expected substrings
207+
// Unwrap to get the root cause error, not just the wrapper
208+
rootErr := err
209+
for {
210+
unwrapped := errors.Unwrap(rootErr)
211+
if unwrapped == nil {
212+
break
213+
}
214+
rootErr = unwrapped
215+
}
216+
rootErrMsg := rootErr.Error()
217+
matchers := make([]types.GomegaMatcher, len(tc.errorSubstrings))
218+
for idx, substr := range tc.errorSubstrings {
219+
matchers[idx] = ContainSubstring(substr)
220+
}
221+
Expect(rootErrMsg).To(Or(matchers...), "root error message should indicate expected issue")
222+
223+
// Verify validation report was not created
224+
reportPath := filepath.Join(validateDir, "report.json")
225+
Expect(reportPath).NotTo(BeAnExistingFile(), "report.json should not be created with malformed JSON")
226+
227+
log.Printf("✅ Test Case %d: Successfully validated error handling for %s", testNum, tc.name)
228+
}()
229+
}
230+
log.Printf("\n========================================")
231+
log.Printf("✅ MTA-860: All malformed API surface file scenarios validated successfully")
232+
})
233+
})

0 commit comments

Comments
 (0)