Skip to content

Commit ea72067

Browse files
ctadakanticharithaT07
authored andcommitted
added end to end ODS lifecycle tests
1 parent c321eae commit ea72067

5 files changed

Lines changed: 355 additions & 319 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ jobs:
6565
working-directory: packages/b2c-cli
6666
run: pnpm run test:ci && pnpm run lint
6767

68+
- name: Run E2E tests
69+
id: e2e-test
70+
if: always() && steps.cli-test.conclusion == 'success' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push')
71+
working-directory: packages/b2c-cli
72+
env:
73+
SFCC_CLIENT_ID: ${{ secrets.SFCC_CLIENT_ID }}
74+
SFCC_CLIENT_SECRET: ${{ secrets.SFCC_CLIENT_SECRET }}
75+
TEST_REALM: ${{ secrets.TEST_REALM }}
76+
SFCC_ACCOUNT_MANAGER_HOST: ${{ secrets.SFCC_ACCOUNT_MANAGER_HOST }}
77+
SFCC_SANDBOX_API_HOST: ${{ secrets.SFCC_SANDBOX_API_HOST }}
78+
run: pnpm --filter @salesforce/b2c-cli run test:e2e
79+
6880
- name: Test Report
6981
uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0
7082
if: always() && steps.sdk-test.conclusion != 'cancelled'

packages/b2c-cli/package.json

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

0 commit comments

Comments
 (0)