Skip to content

Commit 44e88ad

Browse files
authored
W-20878414 SDK unit tests for auth, clients and operations (#40)
* @W-20878414 auth and client Sdk Unit tests * added unit tests for operations * fix lint
1 parent ada38e2 commit 44e88ad

22 files changed

Lines changed: 4655 additions & 3 deletions
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 {ApiKeyStrategy} from '@salesforce/b2c-tooling-sdk/auth';
9+
10+
describe('auth/api-key', () => {
11+
describe('ApiKeyStrategy', () => {
12+
it('getAuthorizationHeader returns Bearer token when headerName is Authorization', async () => {
13+
const auth = new ApiKeyStrategy('my-key', 'Authorization');
14+
const header = await auth.getAuthorizationHeader();
15+
expect(header).to.equal('Bearer my-key');
16+
});
17+
18+
it('getAuthorizationHeader returns raw key when headerName is not Authorization', async () => {
19+
const auth = new ApiKeyStrategy('my-key', 'x-api-key');
20+
const header = await auth.getAuthorizationHeader();
21+
expect(header).to.equal('my-key');
22+
});
23+
24+
it('fetch injects configured header and preserves existing headers', async () => {
25+
const originalFetch = globalThis.fetch;
26+
try {
27+
let seenAuth: string | null = null;
28+
let seenCustom: string | null = null;
29+
30+
globalThis.fetch = (async (_url: string | URL, init?: RequestInit) => {
31+
const headers = new Headers(init?.headers);
32+
seenAuth = headers.get('Authorization');
33+
seenCustom = headers.get('x-custom');
34+
return new Response('ok', {status: 200});
35+
}) as typeof fetch;
36+
37+
const auth = new ApiKeyStrategy('my-key', 'Authorization');
38+
const res = await auth.fetch('https://example.com', {headers: {'x-custom': '1'}});
39+
40+
expect(res.status).to.equal(200);
41+
expect(seenCustom).to.equal('1');
42+
expect(seenAuth).to.equal('Bearer my-key');
43+
} finally {
44+
globalThis.fetch = originalFetch;
45+
}
46+
});
47+
48+
it('fetch injects raw key for non-Authorization headerName', async () => {
49+
const originalFetch = globalThis.fetch;
50+
try {
51+
let seenKey: string | null = null;
52+
53+
globalThis.fetch = (async (_url: string | URL, init?: RequestInit) => {
54+
const headers = new Headers(init?.headers);
55+
seenKey = headers.get('x-api-key');
56+
return new Response('ok', {status: 200});
57+
}) as typeof fetch;
58+
59+
const auth = new ApiKeyStrategy('my-key', 'x-api-key');
60+
await auth.fetch('https://example.com');
61+
62+
expect(seenKey).to.equal('my-key');
63+
} finally {
64+
globalThis.fetch = originalFetch;
65+
}
66+
});
67+
});
68+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 {BasicAuthStrategy} from '@salesforce/b2c-tooling-sdk/auth';
9+
10+
describe('auth/basic', () => {
11+
describe('BasicAuthStrategy', () => {
12+
it('getAuthorizationHeader returns a Basic header with base64 encoded user:pass', async () => {
13+
const auth = new BasicAuthStrategy('user', 'pass');
14+
const header = await auth.getAuthorizationHeader();
15+
expect(header).to.equal(`Basic ${Buffer.from('user:pass').toString('base64')}`);
16+
});
17+
18+
it('fetch injects Authorization header and preserves existing headers', async () => {
19+
const originalFetch = globalThis.fetch;
20+
try {
21+
let seenHeader: string | null = null;
22+
let seenCustom: string | null = null;
23+
24+
globalThis.fetch = (async (_url: string | URL, init?: RequestInit) => {
25+
const headers = new Headers(init?.headers);
26+
seenHeader = headers.get('Authorization');
27+
seenCustom = headers.get('x-custom');
28+
return new Response('ok', {status: 200});
29+
}) as typeof fetch;
30+
31+
const auth = new BasicAuthStrategy('user', 'pass');
32+
const res = await auth.fetch('https://example.com', {headers: {'x-custom': '1'}});
33+
34+
expect(res.status).to.equal(200);
35+
expect(seenCustom).to.equal('1');
36+
expect(seenHeader).to.equal(`Basic ${Buffer.from('user:pass').toString('base64')}`);
37+
} finally {
38+
globalThis.fetch = originalFetch;
39+
}
40+
});
41+
});
42+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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 {
9+
ALL_AUTH_METHODS,
10+
ApiKeyStrategy,
11+
BasicAuthStrategy,
12+
ImplicitOAuthStrategy,
13+
OAuthStrategy,
14+
checkAvailableAuthMethods,
15+
decodeJWT,
16+
resolveAuthStrategy,
17+
} from '@salesforce/b2c-tooling-sdk/auth';
18+
19+
describe('auth/index', () => {
20+
it('exports core strategies and helpers from the auth entrypoint', () => {
21+
expect(ALL_AUTH_METHODS).to.be.an('array');
22+
expect(ApiKeyStrategy).to.be.a('function');
23+
expect(BasicAuthStrategy).to.be.a('function');
24+
expect(ImplicitOAuthStrategy).to.be.a('function');
25+
expect(OAuthStrategy).to.be.a('function');
26+
expect(checkAvailableAuthMethods).to.be.a('function');
27+
expect(resolveAuthStrategy).to.be.a('function');
28+
expect(decodeJWT).to.be.a('function');
29+
});
30+
31+
it('ALL_AUTH_METHODS is stable and can be iterated', () => {
32+
const methods = [...ALL_AUTH_METHODS];
33+
expect(methods).to.include('implicit');
34+
});
35+
});
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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 {ImplicitOAuthStrategy} from '@salesforce/b2c-tooling-sdk/auth';
9+
10+
type TokenResponse = {
11+
accessToken: string;
12+
expires: Date;
13+
scopes: string[];
14+
};
15+
16+
function futureDate(minutes: number): Date {
17+
return new Date(Date.now() + minutes * 60 * 1000);
18+
}
19+
20+
describe('auth/oauth-implicit', () => {
21+
const originalFetch = globalThis.fetch;
22+
23+
afterEach(() => {
24+
globalThis.fetch = originalFetch;
25+
});
26+
27+
it('adds Authorization and x-dw-client-id headers on fetch', async () => {
28+
const clientId = 'implicit-client-headers';
29+
const strategy = new ImplicitOAuthStrategy({clientId, scopes: ['a']});
30+
31+
(strategy as unknown as {implicitFlowLogin: () => Promise<TokenResponse>}).implicitFlowLogin = async () => ({
32+
accessToken: 'tok-1',
33+
expires: futureDate(30),
34+
scopes: ['a'],
35+
});
36+
37+
let seenAuth: string | null = null;
38+
let seenClientId: string | null = null;
39+
40+
globalThis.fetch = async (_input: string | URL | Request, init?: RequestInit) => {
41+
const headers = new Headers(init?.headers);
42+
seenAuth = headers.get('Authorization');
43+
seenClientId = headers.get('x-dw-client-id');
44+
return new Response('ok', {status: 200});
45+
};
46+
47+
const res = await strategy.fetch('https://example.com/test');
48+
expect(res.status).to.equal(200);
49+
expect(seenAuth).to.equal('Bearer tok-1');
50+
expect(seenClientId).to.equal(clientId);
51+
52+
strategy.invalidateToken();
53+
});
54+
55+
it('retries once on 401 after invalidating the cached token', async () => {
56+
const clientId = 'implicit-client-401';
57+
const strategy = new ImplicitOAuthStrategy({clientId, scopes: ['a']});
58+
59+
let tokenCalls = 0;
60+
(strategy as unknown as {implicitFlowLogin: () => Promise<TokenResponse>}).implicitFlowLogin = async () => {
61+
tokenCalls++;
62+
return {
63+
accessToken: tokenCalls === 1 ? 'tok-1' : 'tok-2',
64+
expires: futureDate(30),
65+
scopes: ['a'],
66+
};
67+
};
68+
69+
const seenAuth: string[] = [];
70+
let fetchCalls = 0;
71+
72+
globalThis.fetch = async (_input: string | URL | Request, init?: RequestInit) => {
73+
fetchCalls++;
74+
const headers = new Headers(init?.headers);
75+
seenAuth.push(headers.get('Authorization') ?? '');
76+
77+
if (fetchCalls === 1) {
78+
return new Response('unauthorized', {status: 401});
79+
}
80+
81+
return new Response('ok', {status: 200});
82+
};
83+
84+
const res = await strategy.fetch('https://example.com/test');
85+
expect(res.status).to.equal(200);
86+
87+
expect(fetchCalls).to.equal(2);
88+
expect(tokenCalls).to.equal(2);
89+
expect(seenAuth[0]).to.equal('Bearer tok-1');
90+
expect(seenAuth[1]).to.equal('Bearer tok-2');
91+
92+
strategy.invalidateToken();
93+
});
94+
95+
it('reuses cached token when scopes and expiry are valid', async () => {
96+
const clientId = 'implicit-client-cache';
97+
const strategy = new ImplicitOAuthStrategy({clientId, scopes: ['a']});
98+
99+
let tokenCalls = 0;
100+
(strategy as unknown as {implicitFlowLogin: () => Promise<TokenResponse>}).implicitFlowLogin = async () => {
101+
tokenCalls++;
102+
return {
103+
accessToken: 'tok-cache',
104+
expires: futureDate(30),
105+
scopes: ['a'],
106+
};
107+
};
108+
109+
const t1 = await strategy.getTokenResponse();
110+
const t2 = await strategy.getTokenResponse();
111+
112+
expect(tokenCalls).to.equal(1);
113+
expect(t2.accessToken).to.equal(t1.accessToken);
114+
115+
strategy.invalidateToken();
116+
});
117+
118+
it('re-authenticates when cached token is missing required scopes', async () => {
119+
const clientId = 'implicit-client-scopes';
120+
const strategy = new ImplicitOAuthStrategy({clientId, scopes: ['a', 'b']});
121+
122+
let tokenCalls = 0;
123+
(strategy as unknown as {implicitFlowLogin: () => Promise<TokenResponse>}).implicitFlowLogin = async () => {
124+
tokenCalls++;
125+
return {
126+
accessToken: tokenCalls === 1 ? 'tok-missing-scope' : 'tok-all-scopes',
127+
expires: futureDate(30),
128+
scopes: tokenCalls === 1 ? ['a'] : ['a', 'b'],
129+
};
130+
};
131+
132+
const t1 = await strategy.getTokenResponse();
133+
const t2 = await strategy.getTokenResponse();
134+
135+
expect(tokenCalls).to.equal(2);
136+
expect(t1.accessToken).to.equal('tok-missing-scope');
137+
expect(t2.accessToken).to.equal('tok-all-scopes');
138+
139+
strategy.invalidateToken();
140+
});
141+
142+
it('deduplicates concurrent token requests using pending auth mutex', async () => {
143+
const clientId = 'implicit-client-pending';
144+
const strategy = new ImplicitOAuthStrategy({clientId, scopes: ['a']});
145+
146+
let resolveToken: ((t: TokenResponse) => void) | undefined;
147+
let tokenCalls = 0;
148+
149+
(strategy as unknown as {implicitFlowLogin: () => Promise<TokenResponse>}).implicitFlowLogin = async () => {
150+
tokenCalls++;
151+
return await new Promise<TokenResponse>((resolve) => {
152+
resolveToken = resolve;
153+
});
154+
};
155+
156+
globalThis.fetch = async () => new Response('ok', {status: 200});
157+
158+
const p1 = strategy.fetch('https://example.com/test');
159+
const p2 = strategy.fetch('https://example.com/test');
160+
161+
if (!resolveToken) {
162+
throw new Error('Expected token request to be started');
163+
}
164+
165+
resolveToken({accessToken: 'tok-pending', expires: futureDate(30), scopes: ['a']});
166+
167+
const [r1, r2] = await Promise.all([p1, p2]);
168+
expect(r1.status).to.equal(200);
169+
expect(r2.status).to.equal(200);
170+
expect(tokenCalls).to.equal(1);
171+
172+
strategy.invalidateToken();
173+
});
174+
});

0 commit comments

Comments
 (0)