Skip to content

Commit a61b6f9

Browse files
committed
@W-20683414 initial ODS E2E tests (#1)
* @W-20683414 initial ODS E2E tests * added end to end ODS lifecycle tests * minor refactors * updated e2e test run flow * Updated execa version and lockfile * resolved linting and formatting issues
1 parent cd83263 commit a61b6f9

File tree

7 files changed

+476
-51
lines changed

7 files changed

+476
-51
lines changed

packages/b2c-cli/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"eslint-config-prettier": "^10",
4343
"eslint-plugin-header": "^3.1.1",
4444
"eslint-plugin-prettier": "^5.5.4",
45+
"execa": "^9.6.1",
4546
"mocha": "^10",
4647
"oclif": "^4",
4748
"prettier": "^3.6.2",
@@ -127,9 +128,10 @@
127128
"posttest": "pnpm run lint",
128129
"prepack": "oclif manifest && oclif readme",
129130
"pretest": "tsc --noEmit -p test",
130-
"test": "c8 env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/**/*.test.ts\"",
131-
"test:ci": "c8 env OCLIF_TEST_ROOT=. mocha --forbid-only --reporter json --reporter-option output=test-results.json \"test/**/*.test.ts\"",
132-
"test:unit": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/**/*.test.ts\"",
131+
"test": "c8 env OCLIF_TEST_ROOT=. mocha --forbid-only --exclude \"test/functional/e2e/**\" \"test/**/*.test.ts\"",
132+
"test:ci": "c8 env OCLIF_TEST_ROOT=. mocha --forbid-only --exclude \"test/functional/e2e/**\" --reporter json --reporter-option output=test-results.json \"test/**/*.test.ts\"",
133+
"test:unit": "env OCLIF_TEST_ROOT=. mocha --forbid-only --exclude \"test/functional/e2e/**\" \"test/**/*.test.ts\"",
134+
"test:e2e": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/functional/e2e/**/*.test.ts\"",
133135
"coverage": "c8 report",
134136
"version": "oclif readme && git add README.md",
135137
"dev": "node ./bin/dev.js"
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* SPDX-License-Identifier: Apache-2
4+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
7+
import {expect} from 'chai';
8+
import {execa} from 'execa';
9+
import path from 'node:path';
10+
import {fileURLToPath} from 'node:url';
11+
12+
const __filename = fileURLToPath(import.meta.url);
13+
const __dirname = path.dirname(__filename);
14+
15+
/**
16+
* Helper function to parse JSON response from CLI
17+
*/
18+
function parseJson(output: string): Record<string, unknown> {
19+
try {
20+
// Try to parse the entire output as JSON first
21+
return JSON.parse(output);
22+
} catch {
23+
// If that fails, look for JSON in the output
24+
const lines = output.split('\n');
25+
for (const line of lines) {
26+
const trimmed = line.trim();
27+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
28+
try {
29+
return JSON.parse(trimmed);
30+
} catch {}
31+
}
32+
}
33+
throw new Error(`No valid JSON found in output: ${output}`);
34+
}
35+
}
36+
37+
/**
38+
* E2E Tests for ODS (On-Demand Sandbox) Lifecycle
39+
*
40+
* This test suite covers the complete lifecycle of an ODS sandbox:
41+
* 1. Create sandbox with permissions
42+
* 2. List sandboxes and verify creation
43+
* 3. Deploy code to sandbox
44+
* 4. Stop sandbox
45+
* 5. Start sandbox
46+
* 6. Restart sandbox
47+
* 7. Get sandbox status
48+
* 8. Delete sandbox
49+
*/
50+
describe('ODS Lifecycle E2E Tests', function () {
51+
// Timeout for entire test suite
52+
this.timeout(360_000); // 6 minutes
53+
54+
// Test configuration (paths)
55+
const CLI_BIN = path.resolve(__dirname, '../../../bin/run.js');
56+
const CARTRIDGES_DIR = path.resolve(__dirname, '../fixtures/cartridges');
57+
58+
// Test state
59+
let sandboxId: string;
60+
let serverHostname: string;
61+
62+
before(function () {
63+
// Check required environment variables
64+
if (!process.env.SFCC_CLIENT_ID || !process.env.SFCC_CLIENT_SECRET || !process.env.TEST_REALM) {
65+
this.skip();
66+
}
67+
});
68+
69+
/**
70+
* Helper function to run CLI commands with proper environment.
71+
* Uses process.env directly to get credentials from GitHub secrets.
72+
*/
73+
async function runCLI(args: string[]) {
74+
const result = await execa('node', [CLI_BIN, ...args], {
75+
env: {
76+
...process.env,
77+
SFCC_LOG_LEVEL: 'silent',
78+
},
79+
reject: false,
80+
});
81+
82+
return result;
83+
}
84+
85+
/**
86+
* Helper function to get current sandbox state (for verification only)
87+
*/
88+
async function getSandboxState(sandboxId: string): Promise<null | string> {
89+
const result = await runCLI(['ods', 'get', sandboxId, '--json']);
90+
if (result.exitCode === 0) {
91+
const sandbox = parseJson(result.stdout);
92+
return sandbox.state;
93+
}
94+
return null;
95+
}
96+
97+
describe('Step 1: Create Sandbox', function () {
98+
it('should create a new sandbox with permissions and wait for readiness', async function () {
99+
// --wait can take 5-10 minutes, so increase timeout for this test
100+
this.timeout(600_000); // 6 minutes
101+
102+
const result = await runCLI([
103+
'ods',
104+
'create',
105+
'--realm',
106+
process.env.TEST_REALM!,
107+
'--ttl',
108+
'24',
109+
'--wait',
110+
'--set-permissions',
111+
'--json',
112+
]);
113+
114+
expect(result.exitCode).to.equal(0, `Create command failed: ${result.stderr}`);
115+
expect(result.stdout, 'Create command should return JSON output').to.not.be.empty;
116+
117+
const response = parseJson(result.stdout);
118+
expect(response, 'Create response should be a valid object').to.be.an('object');
119+
expect(response.id, 'Create response should contain a sandbox ID').to.be.a('string').and.not.be.empty;
120+
expect(response.hostName, 'Create response should contain a hostname').to.be.a('string').and.not.be.empty;
121+
expect(response.state, `Sandbox state should be 'started' after --wait, but got '${response.state}'`).to.equal(
122+
'started',
123+
);
124+
125+
// Store for subsequent tests
126+
sandboxId = response.id;
127+
serverHostname = response.hostName;
128+
129+
// Debug output to verify values are set
130+
console.log(`Created sandbox: ${sandboxId} on ${serverHostname}`);
131+
});
132+
});
133+
134+
describe('Step 2: List Sandboxes', function () {
135+
it('should list sandboxes and verify the created one is present', async function () {
136+
// Skip if we don't have a valid sandbox ID
137+
if (!sandboxId) {
138+
this.skip();
139+
}
140+
141+
const result = await runCLI(['ods', 'list', '--realm', process.env.TEST_REALM!, '--json']);
142+
143+
expect(result.exitCode).to.equal(0, `List command failed: ${result.stderr}`);
144+
expect(result.stdout, 'List command should return JSON output').to.not.be.empty;
145+
146+
const response = parseJson(result.stdout);
147+
expect(response, 'List response should be a valid object').to.be.an('object');
148+
expect(response.data, 'List response should contain data array').to.be.an('array');
149+
150+
// Find our sandbox in the list
151+
const foundSandbox = response.data.find((sandbox: Record<string, unknown>) => sandbox.id === sandboxId);
152+
expect(foundSandbox, `Sandbox '${sandboxId}' not found in list.`).to.exist;
153+
expect(foundSandbox.id).to.equal(sandboxId);
154+
});
155+
});
156+
157+
describe('Step 3: Deploy Code', function () {
158+
it('should deploy test cartridge to the sandbox', async function () {
159+
// Skip deploy if we don't have a valid sandbox
160+
if (!sandboxId || !serverHostname) {
161+
this.skip();
162+
}
163+
164+
const result = await runCLI([
165+
'code',
166+
'deploy',
167+
CARTRIDGES_DIR,
168+
'--cartridge',
169+
'plugin_example',
170+
'--server',
171+
serverHostname,
172+
'--account-manager-host',
173+
process.env.SFCC_ACCOUNT_MANAGER_HOST!,
174+
'--json',
175+
]);
176+
177+
expect(result.exitCode).to.equal(0, `Deploy command failed: ${result.stderr}`);
178+
expect(result.stdout, 'Deploy command should return JSON output').to.not.be.empty;
179+
180+
const response = parseJson(result.stdout);
181+
expect(response, 'Deploy response should be a valid object').to.be.an('object');
182+
expect(response.cartridges, 'Deploy response should contain cartridges array')
183+
.to.be.an('array')
184+
.with.length.greaterThan(0);
185+
expect(response.codeVersion, 'Deploy response should contain code version').to.be.a('string').and.not.be.empty;
186+
});
187+
});
188+
189+
describe('Step 4: Stop Sandbox', function () {
190+
it('should stop the sandbox', async function () {
191+
// Skip if we don't have a valid sandbox ID
192+
if (!sandboxId) {
193+
this.skip();
194+
}
195+
196+
const result = await runCLI(['ods', 'stop', sandboxId, '--json']);
197+
198+
expect(result.exitCode).to.equal(0, `Stop command failed: ${result.stderr}`);
199+
200+
const state = await getSandboxState(sandboxId);
201+
if (state) {
202+
expect(
203+
['stopped', 'stopping'],
204+
`Sandbox state should be 'stopped' or 'stopping' after stop command`,
205+
).to.include(state);
206+
}
207+
});
208+
});
209+
210+
describe('Step 5: Start Sandbox', function () {
211+
it('should start the sandbox', async function () {
212+
// Skip if we don't have a valid sandbox ID
213+
if (!sandboxId) {
214+
this.skip();
215+
}
216+
217+
const result = await runCLI(['ods', 'start', sandboxId, '--json']);
218+
219+
expect(result.exitCode).to.equal(0, `Start command failed: ${result.stderr}`);
220+
const state = await getSandboxState(sandboxId);
221+
if (state) {
222+
expect(['started']).to.include(state);
223+
}
224+
});
225+
});
226+
227+
describe('Step 6: Restart Sandbox', function () {
228+
it('should restart the sandbox', async function () {
229+
// Skip if we don't have a valid sandbox ID
230+
if (!sandboxId) {
231+
this.skip();
232+
}
233+
234+
const result = await runCLI(['ods', 'restart', sandboxId, '--json']);
235+
236+
expect(result.exitCode).to.equal(0, `Restart command failed: ${result.stderr}`);
237+
238+
const state = await getSandboxState(sandboxId);
239+
if (state) {
240+
expect(
241+
['started', 'starting', 'restarting'],
242+
`Sandbox state should be 'started', 'starting', or 'restarting' after restart command, but got '${state}'`,
243+
).to.include(state);
244+
}
245+
});
246+
});
247+
248+
describe('Step 7: Get Sandbox Status', function () {
249+
it('should retrieve sandbox status', async function () {
250+
// Skip if we don't have a valid sandbox ID
251+
if (!sandboxId) {
252+
this.skip();
253+
}
254+
255+
const result = await runCLI(['ods', 'get', sandboxId, '--json']);
256+
257+
expect(result.exitCode).to.equal(0, `Get command failed: ${result.stderr}`);
258+
expect(result.stdout, 'Get command should return JSON output').to.not.be.empty;
259+
260+
const response = parseJson(result.stdout);
261+
expect(response, 'Get response should be a valid object').to.be.an('object');
262+
expect(response.id, `Get response ID '${response.id}' should match requested sandbox '${sandboxId}'`).to.equal(
263+
sandboxId,
264+
);
265+
expect(response.state, 'Get response should contain sandbox state').to.be.a('string').and.not.be.empty;
266+
});
267+
});
268+
269+
describe('Step 8: Delete Sandbox', function () {
270+
it('should delete the sandbox', async function () {
271+
// Skip if we don't have a valid sandbox ID
272+
if (!sandboxId) {
273+
this.skip();
274+
}
275+
276+
const result = await runCLI(['ods', 'delete', sandboxId, '--force', '--json']);
277+
278+
expect(result.exitCode).to.equal(0, `Delete command failed: ${result.stderr}`);
279+
});
280+
});
281+
282+
describe('Additional Test Cases', function () {
283+
describe('Error Handling', function () {
284+
it('should handle invalid realm gracefully', async function () {
285+
const result = await runCLI(['ods', 'list', '--realm', 'invalid-realm-xyz', '--json']);
286+
287+
// Command should either succeed with empty list or fail with error
288+
expect(
289+
result.exitCode,
290+
`Invalid realm command should either succeed (0) or fail (1), but got ${result.exitCode}`,
291+
).to.be.oneOf([0, 1]);
292+
});
293+
294+
it('should handle missing sandbox ID gracefully', async function () {
295+
const result = await runCLI(['ods', 'get', 'non-existent-sandbox-id', '--json']);
296+
297+
expect(
298+
result.exitCode,
299+
`Missing sandbox command should fail, but got exit code ${result.exitCode}`,
300+
).to.not.equal(0);
301+
expect(result.stderr, 'Missing sandbox command should return error message').to.not.be.empty;
302+
});
303+
});
304+
305+
describe('Authentication', function () {
306+
it('should fail with invalid credentials', async function () {
307+
const result = await execa('node', [CLI_BIN, 'ods', 'list', '--realm', process.env.TEST_REALM!, '--json'], {
308+
env: {
309+
...process.env,
310+
SFCC_CLIENT_ID: 'invalid-client-id',
311+
SFCC_CLIENT_SECRET: 'invalid-client-secret',
312+
SFCC_LOG_LEVEL: 'silent',
313+
},
314+
reject: false,
315+
});
316+
317+
expect(result.exitCode, `Invalid credentials should fail, but got exit code ${result.exitCode}`).to.not.equal(
318+
0,
319+
);
320+
expect(result.stderr, 'Invalid credentials should return authentication error').to.match(
321+
/401|unauthorized|invalid.*client/i,
322+
);
323+
});
324+
});
325+
});
326+
327+
after(function () {});
328+
});

packages/b2c-cli/test/functional/e2e_cli_test.sh

Lines changed: 0 additions & 40 deletions
This file was deleted.

0 commit comments

Comments
 (0)