Skip to content

Commit 13caadc

Browse files
authored
@W-2068341 adding e2e tests for slas,jobs,webdav,sites,code (#71)
* resolving merge conflicts * cleanup e2e test fixture * update e2e workflow env vars * fixing lint errors * updating slas assertions
1 parent 08e29b1 commit 13caadc

13 files changed

Lines changed: 1907 additions & 4 deletions

.github/workflows/e2e-tests.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ jobs:
5151
TEST_REALM: ${{ vars.TEST_REALM }}
5252
SFCC_ACCOUNT_MANAGER_HOST: ${{ vars.SFCC_ACCOUNT_MANAGER_HOST }}
5353
SFCC_SANDBOX_API_HOST: ${{ vars.SFCC_SANDBOX_API_HOST }}
54+
SFCC_SHORTCODE: ${{ vars.SFCC_SHORTCODE }}
5455
run: |
55-
if [ -n "$SFCC_CLIENT_ID" ] && [ -n "$SFCC_CLIENT_SECRET" ] && [ -n "$TEST_REALM" ] && [ -n "$SFCC_ACCOUNT_MANAGER_HOST" ] && [ -n "$SFCC_SANDBOX_API_HOST" ]; then
56+
if [ -n "$SFCC_CLIENT_ID" ] && [ -n "$SFCC_CLIENT_SECRET" ] && [ -n "$TEST_REALM" ] && [ -n "$SFCC_ACCOUNT_MANAGER_HOST" ] && [ -n "$SFCC_SANDBOX_API_HOST" ] && [ -n "$SFCC_SHORTCODE" ]; then
5657
echo "has-secrets=true" >> $GITHUB_OUTPUT
5758
else
5859
echo "has-secrets=false" >> $GITHUB_OUTPUT
@@ -61,6 +62,7 @@ jobs:
6162
echo " - TEST_REALM (var): ${TEST_REALM:+✓}" >> $GITHUB_STEP_SUMMARY
6263
echo " - SFCC_ACCOUNT_MANAGER_HOST (var): ${SFCC_ACCOUNT_MANAGER_HOST:+✓}" >> $GITHUB_STEP_SUMMARY
6364
echo " - SFCC_SANDBOX_API_HOST (var): ${SFCC_SANDBOX_API_HOST:+✓}" >> $GITHUB_STEP_SUMMARY
65+
echo " - SFCC_SHORTCODE (var): ${SFCC_SHORTCODE:+✓}" >> $GITHUB_STEP_SUMMARY
6466
fi
6567
- name: Setup pnpm
6668
uses: pnpm/action-setup@v4
@@ -97,6 +99,7 @@ jobs:
9799
SFCC_ACCOUNT_MANAGER_HOST: ${{ inputs.sfcc_account_manager_host || vars.SFCC_ACCOUNT_MANAGER_HOST }}
98100
SFCC_SANDBOX_API_HOST: ${{ inputs.sfcc_sandbox_api_host || vars.SFCC_SANDBOX_API_HOST }}
99101
TEST_REALM: ${{ inputs.test_realm || vars.TEST_REALM }}
102+
SFCC_SHORTCODE: ${{ vars.SFCC_SHORTCODE }}
100103
# Test configuration
101104
NODE_ENV: test
102105
SFCC_LOG_LEVEL: silent

packages/b2c-cli/eslint.config.mjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ export default [
1919
// node_modules must be explicitly ignored because the .gitignore pattern only covers
2020
// packages/b2c-cli/node_modules, not the monorepo root node_modules
2121
{
22-
ignores: ['**/node_modules/**', 'test/functional/fixtures/**/*.js', '**/node_modules/marked-terminal/**'],
22+
ignores: [
23+
'**/node_modules/**',
24+
'test/functional/fixtures/**/*.js',
25+
'**/node_modules/marked-terminal/**',
26+
'test/functional/fixtures/**/*.js',
27+
],
2328
},
2429
includeIgnoreFile(gitignorePath),
2530
...oclif,
@@ -35,6 +40,10 @@ export default [
3540
},
3641
rules: {
3742
'header/header': ['error', 'block', copyrightHeader],
43+
// Avoid eslint-plugin-import parsing dependency entrypoints (can stack overflow on CJS bundles)
44+
'import/namespace': 'off',
45+
'import/no-named-as-default-member': 'off',
46+
'import/no-named-as-default': 'off',
3847
...sharedRules,
3948
...oclifRules,
4049
},

packages/b2c-cli/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,14 @@
265265
"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\"",
266266
"test:unit": "env OCLIF_TEST_ROOT=. mocha --forbid-only --exclude \"test/functional/e2e/**\" \"test/**/*.test.ts\"",
267267
"test:agent": "env OCLIF_TEST_ROOT=. mocha --forbid-only --reporter min --exclude \"test/functional/e2e/**\" \"test/**/*.test.ts\"",
268-
"test:e2e": "env OCLIF_TEST_ROOT=. mocha --forbid-only --reporter json --reporter-option output=test-results.json \"test/functional/e2e/**/*.test.ts\"",
268+
"test:e2e": "env TEST_USE_SHARED_SANDBOX=true OCLIF_TEST_ROOT=. mocha --forbid-only --config test/functional/e2e/.mocharc.json \"test/functional/e2e/**/*.test.ts\"",
269+
"test:e2e:auth": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/functional/e2e/auth-token.test.ts\"",
270+
"test:e2e:code": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/functional/e2e/code-lifecycle.test.ts\"",
271+
"test:e2e:jobs": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/functional/e2e/job-execution.test.ts\"",
272+
"test:e2e:ods": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/functional/e2e/ods-lifecycle.test.ts\"",
273+
"test:e2e:sites": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/functional/e2e/sites-operations.test.ts\"",
274+
"test:e2e:slas": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/functional/e2e/slas-lifecycle.test.ts\"",
275+
"test:e2e:webdav": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/functional/e2e/webdav-operations.test.ts\"",
269276
"coverage": "c8 report",
270277
"version": "oclif readme && git add README.md",
271278
"dev": "node ./bin/dev.js"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"node-option": ["import=tsx"],
3+
"timeout": 30000,
4+
"slow": 5000,
5+
"reporter": "spec",
6+
"color": true,
7+
"bail": false,
8+
"require": [
9+
"./test/functional/e2e/hooks.ts"
10+
]
11+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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 Authentication Token Generation
17+
*/
18+
describe('Auth Token E2E Tests', function () {
19+
this.timeout(120_000); // 2 minutes
20+
this.retries(2);
21+
22+
const CLI_BIN = path.resolve(__dirname, '../../../bin/run.js');
23+
24+
before(function () {
25+
if (!process.env.SFCC_CLIENT_ID || !process.env.SFCC_CLIENT_SECRET) {
26+
this.skip();
27+
}
28+
});
29+
30+
async function runCLI(args: string[], env?: Record<string, string>) {
31+
const result = await execa('node', [CLI_BIN, ...args], {
32+
env: {
33+
...process.env,
34+
...env,
35+
SFCC_LOG_LEVEL: 'silent',
36+
},
37+
reject: false,
38+
});
39+
return result;
40+
}
41+
42+
function decodeJWT(token: string): Record<string, unknown> {
43+
const parts = token.split('.');
44+
if (parts.length !== 3) {
45+
throw new Error('Invalid JWT format');
46+
}
47+
const payload = Buffer.from(parts[1], 'base64').toString('utf8');
48+
return JSON.parse(payload);
49+
}
50+
51+
it('should generate a valid OAuth token with correct format, scopes, and expiration', async function () {
52+
const result = await runCLI(['auth:token', '--json']);
53+
expect(result.exitCode).to.equal(0, `Token generation failed: ${result.stderr}`);
54+
expect(result.stdout).to.not.be.empty;
55+
56+
const response = JSON.parse(result.stdout);
57+
expect(response).to.be.an('object');
58+
expect(response.accessToken).to.be.a('string').and.not.be.empty;
59+
expect(response.expires).to.be.a('string');
60+
expect(response.scopes).to.be.an('array').that.is.not.empty;
61+
62+
// Validate JWT format
63+
const payload = decodeJWT(response.accessToken);
64+
expect(payload.sub).to.exist;
65+
expect(payload.exp).to.exist;
66+
67+
// Validate expiration
68+
const now = Math.floor(Date.now() / 1000);
69+
expect(payload.exp as number).to.be.greaterThan(now);
70+
expect((payload.exp as number) - now).to.be.lessThan(86_400);
71+
72+
// Validate expires field matches exp approximately
73+
const expiresDate = new Date(response.expires).getTime() / 1000;
74+
expect(Math.abs(expiresDate - (payload.exp as number))).to.be.lessThan(10);
75+
76+
// Validate scopes
77+
expect(payload.scope, 'Token should contain scope claim').to.exist;
78+
const tokenScopes = Array.isArray(payload.scope) ? payload.scope : (payload.scope as string).split(' ');
79+
for (const s of response.scopes as string[]) {
80+
expect(tokenScopes, `Token should include scope "${s}"`).to.include(s);
81+
}
82+
});
83+
84+
describe('Generate Token With Additional Scopes', function () {
85+
it('should generate a token with allowed additional scopes', async function () {
86+
// Use only scopes your client actually has
87+
const extraScopes = ['profile', 'roles'];
88+
89+
const result = await runCLI(['auth:token', '--scope', extraScopes.join(','), '--json']);
90+
91+
expect(result.exitCode).to.equal(0, `Token generation with extra scopes failed: ${result.stderr}`);
92+
93+
const response = JSON.parse(result.stdout);
94+
const accessToken = response.accessToken as string;
95+
expect(accessToken).to.be.a('string').and.not.be.empty;
96+
expect(response.scopes).to.include.members(extraScopes);
97+
98+
const payload = decodeJWT(accessToken);
99+
expect(payload.scope).to.exist;
100+
101+
const tokenScopes = Array.isArray(payload.scope) ? payload.scope : (payload.scope as string).split(' ');
102+
103+
for (const s of extraScopes) {
104+
expect(tokenScopes, `Token should include scope "${s}"`).to.include(s);
105+
}
106+
107+
console.log(`Token with additional scopes: ${tokenScopes.join(', ')}`);
108+
});
109+
});
110+
111+
describe('Invalid Credentials', function () {
112+
it('should fail with invalid client credentials', async function () {
113+
const result = await runCLI(['auth:token', '--json'], {
114+
SFCC_CLIENT_ID: 'invalid-client-id',
115+
SFCC_CLIENT_SECRET: 'invalid-client-secret',
116+
});
117+
118+
expect(result.exitCode).to.not.equal(0);
119+
expect(result.stderr).to.not.be.empty;
120+
expect(result.stderr).to.match(/401|unauthorized|invalid.*client/i);
121+
});
122+
});
123+
124+
describe('JSON Output Structure', function () {
125+
it('should return correct JSON keys', async function () {
126+
const result = await runCLI(['auth:token', '--json']);
127+
const response = JSON.parse(result.stdout);
128+
expect(response).to.have.all.keys('accessToken', 'expires', 'scopes');
129+
});
130+
});
131+
132+
describe('Default Scopes', function () {
133+
it('should return default scopes when no scopes are requested', async function () {
134+
const result = await runCLI(['auth:token', '--json']);
135+
const response = JSON.parse(result.stdout);
136+
expect(response.scopes.length).to.be.greaterThan(0);
137+
});
138+
});
139+
140+
describe('Non-JSON Output', function () {
141+
it('should output raw token in non-JSON mode', async function () {
142+
const result = await runCLI(['auth:token']);
143+
expect(result.exitCode).to.equal(0);
144+
expect(result.stdout).to.match(/^ey[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/); // JWT regex
145+
});
146+
});
147+
});

0 commit comments

Comments
 (0)