Skip to content

Commit a7bfa4a

Browse files
committed
@W-2068341 adding e2e tests for slas,jobs,webdav,sites,code
1 parent 1420b6f commit a7bfa4a

15 files changed

Lines changed: 2003 additions & 4 deletions

.github/workflows/e2e-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ 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: |
5556
if [ -n "$SFCC_CLIENT_ID" ] && [ -n "$SFCC_CLIENT_SECRET" ] && [ -n "$TEST_REALM" ] && [ -n "$SFCC_ACCOUNT_MANAGER_HOST" ] && [ -n "$SFCC_SANDBOX_API_HOST" ]; then
5657
echo "has-secrets=true" >> $GITHUB_OUTPUT

packages/b2c-cli/eslint.config.mjs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ 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'],
22+
ignores: ['**/node_modules/**', '../node_modules/**', '../../node_modules/**', 'test/functional/fixtures/**/*.js'],
2323
},
2424
includeIgnoreFile(gitignorePath),
2525
...oclif,
@@ -35,8 +35,30 @@ export default [
3535
},
3636
rules: {
3737
'header/header': ['error', 'block', copyrightHeader],
38+
// Avoid eslint-plugin-import parsing dependency entrypoints (can stack overflow on CJS bundles)
39+
'import/namespace': 'off',
40+
'import/no-named-as-default-member': 'off',
41+
'import/no-named-as-default': 'off',
3842
...sharedRules,
3943
...oclifRules,
4044
},
4145
},
46+
{
47+
files: ['test/functional/e2e/**/*.ts'],
48+
rules: {
49+
// E2E tests prioritize end-to-end behavior over stylistic constraints.
50+
'prettier/prettier': 'off',
51+
'unicorn/consistent-function-scoping': 'off',
52+
'unicorn/no-array-for-each': 'off',
53+
'no-await-in-loop': 'off',
54+
'@typescript-eslint/no-explicit-any': 'off',
55+
'unicorn/numeric-separators-style': 'off',
56+
'perfectionist/sort-union-types': 'off',
57+
'no-promise-executor-return': 'off',
58+
'unicorn/import-style': 'off',
59+
'unicorn/text-encoding-identifier-case': 'off',
60+
'object-shorthand': 'off',
61+
'no-undef': 'off',
62+
},
63+
},
4264
];

packages/b2c-cli/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,14 @@
147147
"test": "c8 env OCLIF_TEST_ROOT=. mocha --forbid-only --exclude \"test/functional/e2e/**\" \"test/**/*.test.ts\"",
148148
"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\"",
149149
"test:unit": "env OCLIF_TEST_ROOT=. mocha --forbid-only --exclude \"test/functional/e2e/**\" \"test/**/*.test.ts\"",
150-
"test:e2e": "env OCLIF_TEST_ROOT=. mocha --forbid-only --reporter json --reporter-option output=test-results.json \"test/functional/e2e/**/*.test.ts\"",
150+
"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\"",
151+
"test:e2e:auth": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/functional/e2e/auth-token.test.ts\"",
152+
"test:e2e:code": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/functional/e2e/code-lifecycle.test.ts\"",
153+
"test:e2e:jobs": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/functional/e2e/job-execution.test.ts\"",
154+
"test:e2e:ods": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/functional/e2e/ods-lifecycle.test.ts\"",
155+
"test:e2e:sites": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/functional/e2e/sites-operations.test.ts\"",
156+
"test:e2e:slas": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/functional/e2e/slas-lifecycle.test.ts\"",
157+
"test:e2e:webdav": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/functional/e2e/webdav-operations.test.ts\"",
151158
"coverage": "c8 report",
152159
"version": "oclif readme && git add README.md",
153160
"dev": "node ./bin/dev.js"

packages/b2c-cli/src/commands/docs/read.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66
import {Args, Flags} from '@oclif/core';
77
import {marked} from 'marked';
8-
// eslint-disable-next-line import/namespace
8+
99
import {markedTerminal} from 'marked-terminal';
1010
import {BaseCommand} from '@salesforce/b2c-tooling-sdk/cli';
1111
import {readDocByQuery, type DocEntry} from '@salesforce/b2c-tooling-sdk/operations/docs';
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: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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('utf-8');
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(86400);
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)
79+
? payload.scope
80+
: (payload.scope as string).split(' ');
81+
response.scopes.forEach((s: string) => {
82+
expect(tokenScopes, `Token should include scope "${s}"`).to.include(s);
83+
});
84+
});
85+
86+
describe('Generate Token With Additional Scopes', function () {
87+
it('should generate a token with allowed additional scopes', async function () {
88+
// Use only scopes your client actually has
89+
const extraScopes = ['profile', 'roles'];
90+
91+
const result = await runCLI([
92+
'auth:token',
93+
'--scope',
94+
extraScopes.join(','),
95+
'--json',
96+
]);
97+
98+
expect(result.exitCode).to.equal(
99+
0,
100+
`Token generation with extra scopes failed: ${result.stderr}`
101+
);
102+
103+
const response = JSON.parse(result.stdout);
104+
const accessToken = response.accessToken as string;
105+
expect(accessToken).to.be.a('string').and.not.be.empty;
106+
expect(response.scopes).to.include.members(extraScopes);
107+
108+
const payload = decodeJWT(accessToken);
109+
expect(payload.scope).to.exist;
110+
111+
const tokenScopes = Array.isArray(payload.scope)
112+
? payload.scope
113+
: (payload.scope as string).split(' ');
114+
115+
extraScopes.forEach((s) => {
116+
expect(tokenScopes, `Token should include scope "${s}"`).to.include(s);
117+
});
118+
119+
console.log(`Token with additional scopes: ${tokenScopes.join(', ')}`);
120+
});
121+
});
122+
123+
describe('Invalid Credentials', function () {
124+
it('should fail with invalid client credentials', async function () {
125+
const result = await runCLI(['auth:token', '--json'], {
126+
SFCC_CLIENT_ID: 'invalid-client-id',
127+
SFCC_CLIENT_SECRET: 'invalid-client-secret',
128+
});
129+
130+
expect(result.exitCode).to.not.equal(0);
131+
expect(result.stderr).to.not.be.empty;
132+
expect(result.stderr).to.match(/401|unauthorized|invalid.*client/i);
133+
});
134+
});
135+
136+
describe('JSON Output Structure', function () {
137+
it('should return correct JSON keys', async function () {
138+
const result = await runCLI(['auth:token', '--json']);
139+
const response = JSON.parse(result.stdout);
140+
expect(response).to.have.all.keys('accessToken', 'expires', 'scopes');
141+
});
142+
});
143+
144+
describe('Default Scopes', function () {
145+
it('should return default scopes when no scopes are requested', async function () {
146+
const result = await runCLI(['auth:token', '--json']);
147+
const response = JSON.parse(result.stdout);
148+
expect(response.scopes.length).to.be.greaterThan(0);
149+
});
150+
});
151+
152+
describe('Non-JSON Output', function () {
153+
it('should output raw token in non-JSON mode', async function () {
154+
const result = await runCLI(['auth:token']);
155+
expect(result.exitCode).to.equal(0);
156+
expect(result.stdout).to.match(
157+
/^ey[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/
158+
); // JWT regex
159+
});
160+
});
161+
});

0 commit comments

Comments
 (0)