Skip to content

Commit d8475be

Browse files
authored
@W-20683422 increasing unit test coverage for ODS (#32)
* @W-20683422 increasing unit test coverage for ODS * fix lint and tests * refactored tests * aligning unit tests with stable scope
1 parent 1b4363c commit d8475be

File tree

9 files changed

+2162
-3
lines changed

9 files changed

+2162
-3
lines changed

packages/b2c-cli/src/commands/ods/list.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,15 +142,17 @@ export default class OdsList extends OdsCommand<typeof OdsList> {
142142
},
143143
});
144144

145-
if (!result.data?.data) {
145+
if (result.error) {
146+
const errorResponse = result.error as OdsComponents['schemas']['ErrorResponse'] | undefined;
147+
const errorMessage = errorResponse?.error?.message || result.response?.statusText || 'Unknown error';
146148
this.error(
147149
t('commands.ods.list.error', 'Failed to fetch sandboxes: {{message}}', {
148-
message: result.response?.statusText || 'Unknown error',
150+
message: errorMessage,
149151
}),
150152
);
151153
}
152154

153-
const sandboxes = result.data.data;
155+
const sandboxes = result.data?.data ?? [];
154156
const response: OdsListResponse = {
155157
count: sandboxes.length,
156158
data: sandboxes,
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
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+
/* eslint-disable @typescript-eslint/no-explicit-any, unicorn/consistent-function-scoping */
8+
import {expect} from 'chai';
9+
import OdsCreate from '../../../src/commands/ods/create.js';
10+
import {
11+
makeCommandThrowOnError,
12+
stubCommandConfigAndLogger,
13+
stubOdsClient,
14+
stubResolvedConfig,
15+
} from '../../helpers/ods.js';
16+
17+
/**
18+
* Unit tests for ODS create command CLI logic.
19+
* Tests settings building, permission logic, wait/poll logic.
20+
* SDK tests cover the actual API calls.
21+
*/
22+
describe('ods create', () => {
23+
describe('buildSettings', () => {
24+
it('should return undefined when set-permissions is false', () => {
25+
const command = new OdsCreate([], {} as any);
26+
(command as any).flags = {'set-permissions': false};
27+
28+
// Accessing private method for testing
29+
const settings = (command as any).buildSettings(false);
30+
31+
expect(settings).to.be.undefined;
32+
});
33+
34+
it('should return undefined when no client ID is configured', () => {
35+
const command = new OdsCreate([], {} as any);
36+
stubCommandConfigAndLogger(command);
37+
stubResolvedConfig(command, {});
38+
39+
const settings = (command as any).buildSettings(true);
40+
41+
expect(settings).to.be.undefined;
42+
});
43+
44+
it('should build settings with OCAPI and WebDAV permissions', () => {
45+
const command = new OdsCreate([], {} as any);
46+
stubCommandConfigAndLogger(command);
47+
stubResolvedConfig(command, {clientId: 'test-client-id'});
48+
49+
const settings = (command as any).buildSettings(true);
50+
51+
expect(settings).to.exist;
52+
expect(settings).to.have.property('ocapi');
53+
expect(settings).to.have.property('webdav');
54+
expect(settings.ocapi).to.be.an('array').with.length.greaterThan(0);
55+
expect(settings.webdav).to.be.an('array').with.length.greaterThan(0);
56+
expect(settings.ocapi[0]).to.have.property('client_id');
57+
expect(settings.webdav[0]).to.have.property('client_id');
58+
});
59+
60+
it('should include default OCAPI resources', () => {
61+
const command = new OdsCreate([], {} as any);
62+
stubCommandConfigAndLogger(command);
63+
stubResolvedConfig(command, {clientId: 'test-client-id'});
64+
65+
const settings = (command as any).buildSettings(true);
66+
67+
const resources = settings.ocapi[0].resources;
68+
expect(resources).to.be.an('array');
69+
expect(resources.some((r: any) => r.resource_id === '/code_versions')).to.be.true;
70+
expect(resources.some((r: any) => r.resource_id.includes('/jobs/'))).to.be.true;
71+
});
72+
73+
it('should include default WebDAV permissions', () => {
74+
const command = new OdsCreate([], {} as any);
75+
stubCommandConfigAndLogger(command);
76+
stubResolvedConfig(command, {clientId: 'test-client-id'});
77+
78+
const settings = (command as any).buildSettings(true);
79+
80+
const permissions = settings.webdav[0].permissions;
81+
expect(permissions).to.be.an('array');
82+
expect(permissions.some((p: any) => p.path === '/impex')).to.be.true;
83+
expect(permissions.some((p: any) => p.path === '/cartridges')).to.be.true;
84+
});
85+
});
86+
87+
describe('flag defaults', () => {
88+
it('should have correct default TTL', () => {
89+
expect(OdsCreate.flags.ttl.default).to.equal(24);
90+
});
91+
92+
it('should have correct default profile', () => {
93+
expect(OdsCreate.flags.profile.default).to.equal('medium');
94+
});
95+
96+
it('should have correct default for set-permissions', () => {
97+
expect(OdsCreate.flags['set-permissions'].default).to.equal(true);
98+
});
99+
100+
it('should have correct default for auto-scheduled', () => {
101+
expect(OdsCreate.flags['auto-scheduled'].default).to.equal(false);
102+
});
103+
104+
it('should have correct default for wait', () => {
105+
expect(OdsCreate.flags.wait.default).to.equal(false);
106+
});
107+
108+
it('should have correct default poll interval', () => {
109+
expect(OdsCreate.flags['poll-interval'].default).to.equal(10);
110+
});
111+
112+
it('should have correct default timeout', () => {
113+
expect(OdsCreate.flags.timeout.default).to.equal(600);
114+
});
115+
});
116+
117+
describe('profile options', () => {
118+
it('should only allow valid profile values', () => {
119+
const validProfiles = ['medium', 'large', 'xlarge', 'xxlarge'];
120+
expect(OdsCreate.flags.profile.options).to.deep.equal(validProfiles);
121+
});
122+
});
123+
124+
describe('run()', () => {
125+
function setupCreateCommand(): OdsCreate {
126+
const command = new OdsCreate([], {} as any);
127+
128+
stubCommandConfigAndLogger(command);
129+
130+
// Mock log & error
131+
command.log = () => {};
132+
makeCommandThrowOnError(command);
133+
134+
return command;
135+
}
136+
137+
it('should create sandbox successfully without wait', async () => {
138+
const command = setupCreateCommand();
139+
140+
(command as any).flags = {
141+
realm: 'abcd',
142+
ttl: 24,
143+
profile: 'medium',
144+
'auto-scheduled': false,
145+
wait: false,
146+
'set-permissions': false,
147+
json: true,
148+
};
149+
150+
stubOdsClient(command, {
151+
POST: async () => ({
152+
data: {
153+
data: {
154+
id: 'sb-123',
155+
realm: 'abcd',
156+
state: 'creating',
157+
},
158+
},
159+
}),
160+
});
161+
162+
const result = await command.run();
163+
164+
expect(result.id).to.equal('sb-123');
165+
});
166+
167+
it('should throw error when sandbox creation fails', async () => {
168+
const command = setupCreateCommand();
169+
170+
(command as any).flags = {
171+
realm: 'abcd',
172+
ttl: 24,
173+
profile: 'medium',
174+
wait: false,
175+
'set-permissions': false,
176+
};
177+
178+
stubOdsClient(command, {
179+
POST: async () => ({
180+
data: undefined,
181+
error: {
182+
error: {message: 'Invalid realm'},
183+
},
184+
response: {
185+
statusText: 'Bad Request',
186+
},
187+
}),
188+
});
189+
190+
try {
191+
await command.run();
192+
expect.fail('Expected error');
193+
} catch (error: any) {
194+
expect(error.message).to.include('Failed to create sandbox');
195+
}
196+
});
197+
198+
it('should not include settings when set-permissions is false', async () => {
199+
const command = setupCreateCommand();
200+
201+
(command as any).flags = {
202+
realm: 'abcd',
203+
ttl: 24,
204+
profile: 'medium',
205+
wait: false,
206+
'set-permissions': false,
207+
};
208+
209+
let requestBody: any;
210+
211+
stubOdsClient(command, {
212+
async POST(_url: string, options: any) {
213+
requestBody = options.body;
214+
return {
215+
data: {data: {id: 'sb-1', state: 'creating'}},
216+
};
217+
},
218+
});
219+
220+
await command.run();
221+
222+
expect(requestBody.settings).to.be.undefined;
223+
});
224+
225+
describe('waitForSandbox()', () => {
226+
it('should wait until sandbox reaches started state', async () => {
227+
const command = setupCreateCommand();
228+
let calls = 0;
229+
230+
const mockClient = {
231+
async GET() {
232+
calls++;
233+
return {
234+
data: {
235+
data: {
236+
state: calls < 2 ? 'creating' : 'started',
237+
},
238+
},
239+
};
240+
},
241+
};
242+
243+
stubOdsClient(command, mockClient);
244+
245+
const result = await (command as any).waitForSandbox('sb-1', 0, 5);
246+
247+
expect(result.state).to.equal('started');
248+
});
249+
250+
it('should error when sandbox enters failed state', async () => {
251+
const command = setupCreateCommand();
252+
253+
stubOdsClient(command, {
254+
GET: async () => ({
255+
data: {data: {state: 'failed'}},
256+
}),
257+
});
258+
259+
try {
260+
await (command as any).waitForSandbox('sb-1', 0, 5);
261+
expect.fail('Expected error');
262+
} catch (error: any) {
263+
expect(error.message).to.include('Sandbox creation failed');
264+
}
265+
});
266+
267+
it('should error when sandbox is deleted', async () => {
268+
const command = setupCreateCommand();
269+
270+
stubOdsClient(command, {
271+
GET: async () => ({
272+
data: {data: {state: 'deleted'}},
273+
}),
274+
});
275+
276+
try {
277+
await (command as any).waitForSandbox('sb-1', 0, 5);
278+
expect.fail('Expected error');
279+
} catch (error: any) {
280+
expect(error.message).to.include('Sandbox was deleted');
281+
}
282+
});
283+
284+
it('should timeout if sandbox never reaches terminal state', async () => {
285+
const command = setupCreateCommand();
286+
287+
stubOdsClient(command, {
288+
GET: async () => ({
289+
data: {data: {state: 'creating'}},
290+
}),
291+
});
292+
293+
try {
294+
await (command as any).waitForSandbox('sb-1', 0, 1);
295+
expect.fail('Expected timeout');
296+
} catch (error: any) {
297+
expect(error.message).to.include('Timeout waiting for sandbox');
298+
}
299+
});
300+
301+
it('should error if polling API returns no data', async () => {
302+
const command = setupCreateCommand();
303+
304+
stubOdsClient(command, {
305+
GET: async () => ({
306+
data: undefined,
307+
response: {statusText: 'Internal Error'},
308+
}),
309+
});
310+
311+
try {
312+
await (command as any).waitForSandbox('sb-1', 0, 5);
313+
expect.fail('Expected error');
314+
} catch (error: any) {
315+
expect(error.message).to.include('Failed to fetch sandbox status');
316+
}
317+
});
318+
});
319+
});
320+
});

0 commit comments

Comments
 (0)