Skip to content

Commit 8826487

Browse files
@W-20893693: Adding AM topic with users, role and org subtopics
1 parent 629a395 commit 8826487

5 files changed

Lines changed: 191 additions & 72 deletions

File tree

packages/b2c-cli/test/commands/am/orgs/audit.test.ts

Lines changed: 23 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,15 @@ import {http, HttpResponse} from 'msw';
1010
import {setupServer} from 'msw/node';
1111
import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils';
1212
import OrgAudit from '../../../../src/commands/am/orgs/audit.js';
13-
import {stubCommandConfigAndLogger, stubJsonEnabled, makeCommandThrowOnError} from '../../../helpers/test-setup.js';
13+
import {
14+
stubCommandConfigAndLogger,
15+
stubJsonEnabled,
16+
makeCommandThrowOnError,
17+
stubImplicitOAuthStrategy,
18+
} from '../../../helpers/test-setup.js';
1419

1520
const TEST_HOST = 'account.test.demandware.com';
1621
const BASE_URL = `https://${TEST_HOST}/dw/rest/v1`;
17-
const OAUTH_URL = `https://${TEST_HOST}/dwsso/oauth2/access_token`;
18-
19-
function createMockJWT(payload: Record<string, unknown> = {}): string {
20-
const header = {alg: 'HS256', typ: 'JWT'};
21-
const defaultPayload = {sub: 'test-client', iat: Math.floor(Date.now() / 1000), ...payload};
22-
const headerB64 = Buffer.from(JSON.stringify(header)).toString('base64url');
23-
const payloadB64 = Buffer.from(JSON.stringify(defaultPayload)).toString('base64url');
24-
return `${headerB64}.${payloadB64}.signature`;
25-
}
2622

2723
/**
2824
* Unit tests for org audit command CLI logic.
@@ -103,15 +99,10 @@ describe('org audit', () => {
10399

104100
stubCommandConfigAndLogger(command);
105101
stubJsonEnabled(command, true);
102+
// Mock implicit OAuth strategy to avoid browser-based flow
103+
stubImplicitOAuthStrategy(command);
106104

107105
server.use(
108-
http.post(OAUTH_URL, () => {
109-
return HttpResponse.json({
110-
access_token: createMockJWT({sub: 'test-client'}),
111-
expires_in: 1800,
112-
scope: 'sfcc.accountmanager.user.manage',
113-
});
114-
}),
115106
http.get(`${BASE_URL}/organizations/org-123`, () => {
116107
return HttpResponse.json(mockOrg);
117108
}),
@@ -140,15 +131,10 @@ describe('org audit', () => {
140131
(command as any).flags = {};
141132
stubCommandConfigAndLogger(command);
142133
stubJsonEnabled(command, false);
134+
// Mock implicit OAuth strategy to avoid browser-based flow
135+
stubImplicitOAuthStrategy(command);
143136

144137
server.use(
145-
http.post(OAUTH_URL, () => {
146-
return HttpResponse.json({
147-
access_token: createMockJWT({sub: 'test-client'}),
148-
expires_in: 1800,
149-
scope: 'sfcc.accountmanager.user.manage',
150-
});
151-
}),
152138
http.get(`${BASE_URL}/organizations/org-123`, () => {
153139
return HttpResponse.json(mockOrg);
154140
}),
@@ -173,15 +159,10 @@ describe('org audit', () => {
173159

174160
stubCommandConfigAndLogger(command);
175161
stubJsonEnabled(command, true);
162+
// Mock implicit OAuth strategy to avoid browser-based flow
163+
stubImplicitOAuthStrategy(command);
176164

177165
server.use(
178-
http.post(OAUTH_URL, () => {
179-
return HttpResponse.json({
180-
access_token: createMockJWT({sub: 'test-client'}),
181-
expires_in: 1800,
182-
scope: 'sfcc.accountmanager.user.manage',
183-
});
184-
}),
185166
http.get(`${BASE_URL}/organizations/org-123`, () => {
186167
return HttpResponse.json(mockOrg);
187168
}),
@@ -206,14 +187,10 @@ describe('org audit', () => {
206187
stubCommandConfigAndLogger(command);
207188
stubJsonEnabled(command, true);
208189

190+
// Mock implicit OAuth strategy to avoid browser-based flow
191+
stubImplicitOAuthStrategy(command);
192+
209193
server.use(
210-
http.post(OAUTH_URL, () => {
211-
return HttpResponse.json({
212-
access_token: createMockJWT({sub: 'test-client'}),
213-
expires_in: 1800,
214-
scope: 'sfcc.accountmanager.user.manage',
215-
});
216-
}),
217194
http.get(`${BASE_URL}/organizations/Test%20Organization`, () => {
218195
return HttpResponse.json({error: {message: 'Not found'}}, {status: 404});
219196
}),
@@ -241,15 +218,10 @@ describe('org audit', () => {
241218

242219
stubCommandConfigAndLogger(command);
243220
makeCommandThrowOnError(command);
221+
// Mock implicit OAuth strategy to avoid browser-based flow
222+
stubImplicitOAuthStrategy(command);
244223

245224
server.use(
246-
http.post(OAUTH_URL, () => {
247-
return HttpResponse.json({
248-
access_token: createMockJWT({sub: 'test-client'}),
249-
expires_in: 1800,
250-
scope: 'sfcc.accountmanager.user.manage',
251-
});
252-
}),
253225
http.get(`${BASE_URL}/organizations/nonexistent-org`, () => {
254226
return HttpResponse.json({error: {message: 'Not found'}}, {status: 404});
255227
}),
@@ -278,15 +250,10 @@ describe('org audit', () => {
278250
(command as any).flags = {columns: 'timestamp,authorDisplayName,eventType'};
279251
stubCommandConfigAndLogger(command);
280252
stubJsonEnabled(command, false);
253+
// Mock implicit OAuth strategy to avoid browser-based flow
254+
stubImplicitOAuthStrategy(command);
281255

282256
server.use(
283-
http.post(OAUTH_URL, () => {
284-
return HttpResponse.json({
285-
access_token: createMockJWT({sub: 'test-client'}),
286-
expires_in: 1800,
287-
scope: 'sfcc.accountmanager.user.manage',
288-
});
289-
}),
290257
http.get(`${BASE_URL}/organizations/org-123`, () => {
291258
return HttpResponse.json(mockOrg);
292259
}),
@@ -312,15 +279,10 @@ describe('org audit', () => {
312279
(command as any).flags = {extended: true};
313280
stubCommandConfigAndLogger(command);
314281
stubJsonEnabled(command, false);
282+
// Mock implicit OAuth strategy to avoid browser-based flow
283+
stubImplicitOAuthStrategy(command);
315284

316285
server.use(
317-
http.post(OAUTH_URL, () => {
318-
return HttpResponse.json({
319-
access_token: createMockJWT({sub: 'test-client'}),
320-
expires_in: 1800,
321-
scope: 'sfcc.accountmanager.user.manage',
322-
});
323-
}),
324286
http.get(`${BASE_URL}/organizations/org-123`, () => {
325287
return HttpResponse.json(mockOrg);
326288
}),
@@ -345,6 +307,8 @@ describe('org audit', () => {
345307

346308
stubCommandConfigAndLogger(command);
347309
stubJsonEnabled(command, true);
310+
// Mock implicit OAuth strategy to avoid browser-based flow
311+
stubImplicitOAuthStrategy(command);
348312

349313
const logsWithTimestamps = [
350314
{
@@ -357,13 +321,6 @@ describe('org audit', () => {
357321
];
358322

359323
server.use(
360-
http.post(OAUTH_URL, () => {
361-
return HttpResponse.json({
362-
access_token: createMockJWT({sub: 'test-client'}),
363-
expires_in: 1800,
364-
scope: 'sfcc.accountmanager.user.manage',
365-
});
366-
}),
367324
http.get(`${BASE_URL}/organizations/org-123`, () => {
368325
return HttpResponse.json(mockOrg);
369326
}),

packages/b2c-cli/test/helpers/test-setup.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,19 @@ import type {Config} from '@oclif/core';
88
import {captureOutput} from '@oclif/test';
99
import sinon from 'sinon';
1010
import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils';
11+
import {ImplicitOAuthStrategy} from '@salesforce/b2c-tooling-sdk/auth';
1112
import {stubParse} from './stub-parse.js';
1213

14+
type TokenResponse = {
15+
accessToken: string;
16+
expires: Date;
17+
scopes: string[];
18+
};
19+
20+
function futureDate(minutes: number): Date {
21+
return new Date(Date.now() + minutes * 60 * 1000);
22+
}
23+
1324
/**
1425
* Run a command silently, capturing stdout/stderr.
1526
* Use this when you don't need to verify console output.
@@ -137,3 +148,29 @@ export function makeCommandThrowOnError(command: any): void {
137148
throw new Error(msg);
138149
};
139150
}
151+
152+
/**
153+
* Mocks getOAuthStrategy to return ImplicitOAuthStrategy with mocked implicitFlowLogin.
154+
* This follows the pattern from oauth-implicit.test.ts to avoid browser-based OAuth flow.
155+
* Use this for AM command tests that need to test implicit flow behavior without triggering
156+
* the interactive browser-based authentication.
157+
*
158+
* @param command - The command instance to stub
159+
* @param accountManagerHost - Account Manager hostname (default: 'account.test.demandware.com')
160+
*/
161+
export function stubImplicitOAuthStrategy(command: any, accountManagerHost = 'account.test.demandware.com'): void {
162+
const strategy = new ImplicitOAuthStrategy({
163+
clientId: 'test-client-id',
164+
accountManagerHost,
165+
});
166+
167+
// Mock implicitFlowLogin to avoid browser-based OAuth flow (following oauth-implicit.test.ts pattern)
168+
(strategy as unknown as {implicitFlowLogin: () => Promise<TokenResponse>}).implicitFlowLogin = async () => ({
169+
accessToken: 'test-token',
170+
expires: futureDate(30),
171+
scopes: [],
172+
});
173+
174+
// Stub getOAuthStrategy to return our mocked strategy
175+
sinon.stub(command as {getOAuthStrategy: () => typeof strategy}, 'getOAuthStrategy').returns(strategy);
176+
}

packages/b2c-tooling-sdk/src/cli/am-command.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,56 @@ import {Command} from '@oclif/core';
77
import {OAuthCommand} from './oauth-command.js';
88
import {createAccountManagerClient} from '../clients/am-api.js';
99
import type {AccountManagerClient} from '../clients/am-api.js';
10+
import type {AuthMethod} from './config.js';
1011

1112
/**
1213
* Base command for Account Manager operations.
1314
*
1415
* Extends OAuthCommand with Account Manager client setup for users, roles, and organizations.
16+
* Overrides default auth methods to prioritize implicit flow for Account Manager operations.
1517
*
1618
* @example
1719
* export default class UserList extends AmCommand<typeof UserList> {
1820
* async run(): Promise<void> {
19-
* const users = await listUsers(this.accountManagerUsersClient, {});
21+
* const users = await this.accountManagerClient.listUsers({});
2022
* // ...
2123
* }
2224
* }
2325
*
2426
* @example
2527
* export default class OrgList extends AmCommand<typeof OrgList> {
2628
* async run(): Promise<void> {
27-
* const orgs = await this.accountManagerOrgsClient.listOrgs();
29+
* const orgs = await this.accountManagerClient.listOrgs();
2830
* // ...
2931
* }
3032
* }
3133
*/
3234
export abstract class AmCommand<T extends typeof Command> extends OAuthCommand<T> {
35+
/**
36+
* Override default auth methods to prioritize implicit flow for Account Manager.
37+
* Gets the default methods from parent class, then ensures 'implicit' is first.
38+
* If 'implicit' is already present, moves it to first position.
39+
* If 'implicit' is not present, prepends it to the beginning.
40+
*/
41+
protected override getDefaultAuthMethods(): AuthMethod[] {
42+
const defaultMethods = super.getDefaultAuthMethods();
43+
const implicitIndex = defaultMethods.indexOf('implicit');
44+
45+
if (implicitIndex === 0) {
46+
// Already first, return as-is
47+
return defaultMethods;
48+
}
49+
50+
if (implicitIndex > 0) {
51+
// Implicit exists but not first - move it to first
52+
const methods = [...defaultMethods];
53+
methods.splice(implicitIndex, 1);
54+
return ['implicit', ...methods];
55+
}
56+
57+
// Implicit not present - prepend it
58+
return ['implicit', ...defaultMethods];
59+
}
3360
private _accountManagerClient?: AccountManagerClient;
3461

3562
/**

packages/b2c-tooling-sdk/src/cli/oauth-command.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ import {ImplicitOAuthStrategy} from '../auth/oauth-implicit.js';
1313
import {t} from '../i18n/index.js';
1414
import {DEFAULT_ACCOUNT_MANAGER_HOST} from '../defaults.js';
1515

16+
/**
17+
* Default OAuth authentication methods array used by getOAuthStrategy.
18+
* Extracted from getOAuthStrategy() to ensure getDefaultAuthMethods() returns the same array.
19+
*/
20+
const DEFAULT_OAUTH_AUTH_METHODS: AuthMethod[] = ['client-credentials', 'implicit'];
21+
1622
/**
1723
* Base command for operations requiring OAuth authentication.
1824
* Use this for platform-level operations like ODS, APIs.
@@ -85,6 +91,17 @@ export abstract class OAuthCommand<T extends typeof Command> extends BaseCommand
8591
return this.resolvedConfig.values.accountManagerHost ?? DEFAULT_ACCOUNT_MANAGER_HOST;
8692
}
8793

94+
/**
95+
* Gets the default authentication methods in priority order.
96+
* This method is used by getOAuthStrategy() when no auth methods are specified in config.
97+
* Subclasses can override this to change the default priority.
98+
*
99+
* @returns Array of auth methods in priority order (first is highest priority)
100+
*/
101+
protected getDefaultAuthMethods(): AuthMethod[] {
102+
return DEFAULT_OAUTH_AUTH_METHODS;
103+
}
104+
88105
/**
89106
* Gets an OAuth auth strategy based on allowed auth methods and available credentials.
90107
*
@@ -96,8 +113,8 @@ export abstract class OAuthCommand<T extends typeof Command> extends BaseCommand
96113
protected getOAuthStrategy(): OAuthStrategy | ImplicitOAuthStrategy {
97114
const config = this.resolvedConfig.values;
98115
const accountManagerHost = this.accountManagerHost;
99-
// Default to client-credentials and implicit if no methods specified
100-
const allowedMethods = config.authMethods || (['client-credentials', 'implicit'] as AuthMethod[]);
116+
// Use getDefaultAuthMethods() to get default array, allowing subclasses to override
117+
const allowedMethods = config.authMethods || this.getDefaultAuthMethods();
101118

102119
for (const method of allowedMethods) {
103120
switch (method) {

0 commit comments

Comments
 (0)