Skip to content

Commit 2129b15

Browse files
author
Kanishka
committed
feat: add sandbox configuration update API
- Add PATCH /configurations/sandbox endpoint for updating sandbox audit configurations - Implement updateSandboxConfig method in ConfigurationController with admin access control
1 parent 88cc99c commit 2129b15

File tree

6 files changed

+574
-4159
lines changed

6 files changed

+574
-4159
lines changed

package-lock.json

Lines changed: 231 additions & 4158 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
"@adobe/helix-universal-logger": "3.0.27",
7474
"@adobe/spacecat-shared-athena-client": "1.3.0",
7575
"@adobe/spacecat-shared-brand-client": "1.1.20",
76-
"@adobe/spacecat-shared-data-access": "2.58.0",
76+
"@adobe/spacecat-shared-data-access": "https://gitpkg.now.sh/adobe/spacecat-shared/packages/spacecat-shared-data-access?feature/site-sandbox-configuration",
7777
"@adobe/spacecat-shared-gpt-client": "1.5.21",
7878
"@adobe/spacecat-shared-http-utils": "1.16.0",
7979
"@adobe/spacecat-shared-ims-client": "1.8.8",

src/controllers/configuration.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,49 @@ function ConfigurationController(ctx) {
8888
return ok(ConfigurationDto.toJSON(configuration));
8989
};
9090

91+
/**
92+
* Updates sandbox configuration for audit types.
93+
* @param {UniversalContext} context - Context of the request.
94+
* @return {Promise<Response>} Update result response.
95+
*/
96+
const updateSandboxConfig = async (context) => {
97+
if (!accessControlUtil.hasAdminAccess()) {
98+
return forbidden('Only admins can update sandbox configurations');
99+
}
100+
101+
const { sandboxConfigs } = context.data || {};
102+
103+
if (!sandboxConfigs || typeof sandboxConfigs !== 'object') {
104+
return badRequest('sandboxConfigs object is required');
105+
}
106+
107+
try {
108+
// Load latest configuration
109+
const config = await Configuration.findLatest();
110+
if (!config) {
111+
return notFound('Configuration not found');
112+
}
113+
114+
// Update sandbox configurations
115+
const updatedConfig = await config.updateSandboxAuditConfigs(sandboxConfigs);
116+
// Save the updated configuration
117+
await Configuration.save(updatedConfig);
118+
119+
return ok({
120+
message: 'Sandbox configurations updated successfully',
121+
updatedConfigs: sandboxConfigs,
122+
totalUpdated: Object.keys(sandboxConfigs).length,
123+
});
124+
} catch (error) {
125+
return badRequest(`Error updating sandbox configuration: ${error.message}`);
126+
}
127+
};
128+
91129
return {
92130
getAll,
93131
getByVersion,
94132
getLatest,
133+
updateSandboxConfig,
95134
};
96135
}
97136

src/routes/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export default function getRouteHandlers(
111111
'GET /configurations/latest': configurationController.getLatest,
112112
'PUT /configurations/latest': configurationController.updateConfiguration,
113113
'GET /configurations/:version': configurationController.getByVersion,
114+
'PATCH /configurations/sandbox': configurationController.updateSandboxConfig,
114115
'PATCH /configurations/sites/audits': sitesAuditsToggleController.execute,
115116
'POST /event/fulfillment': fulfillmentController.processFulfillmentEvents,
116117
'POST /event/fulfillment/:eventType': fulfillmentController.processFulfillmentEvents,

test/controllers/configurations.test.js

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ describe('Configurations Controller', () => {
8888
'getAll',
8989
'getLatest',
9090
'getByVersion',
91+
'updateSandboxConfig',
9192
];
9293

9394
let mockDataAccess;
@@ -235,4 +236,302 @@ describe('Configurations Controller', () => {
235236
expect(result.status).to.equal(400);
236237
expect(error).to.have.property('message', 'Configuration version required to be an integer');
237238
});
239+
240+
describe('Sandbox Configuration Methods', () => {
241+
let mockCurrentConfig;
242+
let mockNewConfig;
243+
244+
beforeEach(() => {
245+
// Reset admin access for sandbox configuration tests
246+
context.attributes.authInfo.withProfile({ is_admin: true });
247+
mockCurrentConfig = {
248+
getJobs: sandbox.stub().returns([]),
249+
getHandlers: sandbox.stub().returns({}),
250+
getQueues: sandbox.stub().returns({}),
251+
getSlackRoles: sandbox.stub().returns({}),
252+
};
253+
254+
mockNewConfig = {
255+
getVersion: sandbox.stub().returns('2'),
256+
getJobs: sandbox.stub().returns([]),
257+
getHandlers: sandbox.stub().returns({}),
258+
getQueues: sandbox.stub().returns({}),
259+
getSlackRoles: sandbox.stub().returns({}),
260+
};
261+
262+
mockDataAccess.Configuration.findLatest = sandbox.stub().resolves(mockCurrentConfig);
263+
mockDataAccess.Configuration.create = sandbox.stub().resolves(mockNewConfig);
264+
});
265+
266+
describe('updateSandboxConfig', () => {
267+
it('should update sandbox configurations successfully', async () => {
268+
const requestContext = {
269+
data: {
270+
sandboxConfigs: {
271+
cwv: { expire: '10' },
272+
'meta-tags': { expire: '15' },
273+
},
274+
},
275+
};
276+
277+
const result = await configurationsController.updateSandboxConfig(requestContext);
278+
const response = await result.json();
279+
280+
expect(result.status).to.equal(200);
281+
expect(response).to.have.property('message', 'Sandbox configurations updated successfully');
282+
expect(response).to.have.property('updatedConfigs');
283+
expect(response.updatedConfigs).to.deep.equal({
284+
cwv: { expire: '10' },
285+
'meta-tags': { expire: '15' },
286+
});
287+
expect(response).to.have.property('totalUpdated', 2);
288+
expect(response).to.have.property('newVersion', '2');
289+
expect(mockDataAccess.Configuration.create).to.have.been.calledWith({
290+
jobs: [],
291+
handlers: {
292+
cwv: { sandbox: { expire: '10' } },
293+
'meta-tags': { sandbox: { expire: '15' } },
294+
},
295+
queues: {},
296+
slackRoles: {},
297+
});
298+
});
299+
300+
it('should return bad request when sandboxConfigs is missing', async () => {
301+
const requestContext = {
302+
data: {},
303+
};
304+
305+
const result = await configurationsController.updateSandboxConfig(requestContext);
306+
const error = await result.json();
307+
308+
expect(result.status).to.equal(400);
309+
expect(error.message).to.include('sandboxConfigs object is required');
310+
});
311+
312+
it('should return bad request when context.data is undefined', async () => {
313+
const requestContext = {};
314+
315+
const result = await configurationsController.updateSandboxConfig(requestContext);
316+
const error = await result.json();
317+
318+
expect(result.status).to.equal(400);
319+
expect(error.message).to.include('sandboxConfigs object is required');
320+
});
321+
322+
it('should return bad request when sandboxConfigs is not an object', async () => {
323+
const requestContext = {
324+
data: {
325+
sandboxConfigs: 'invalid',
326+
},
327+
};
328+
329+
const result = await configurationsController.updateSandboxConfig(requestContext);
330+
const error = await result.json();
331+
332+
expect(result.status).to.equal(400);
333+
expect(error.message).to.include('sandboxConfigs object is required');
334+
});
335+
336+
it('should return forbidden for non-admin users', async () => {
337+
context.attributes.authInfo.withProfile({ is_admin: false });
338+
339+
const requestContext = {
340+
data: {
341+
sandboxConfigs: {
342+
cwv: { expire: '10' },
343+
},
344+
},
345+
};
346+
347+
const result = await configurationsController.updateSandboxConfig(requestContext);
348+
const error = await result.json();
349+
350+
expect(result.status).to.equal(403);
351+
expect(error.message).to.include('Only admins can update sandbox configurations');
352+
});
353+
354+
it('should return not found when configuration does not exist', async () => {
355+
mockDataAccess.Configuration.findLatest.resolves(null);
356+
357+
const requestContext = {
358+
data: {
359+
sandboxConfigs: {
360+
cwv: { expire: '10' },
361+
},
362+
},
363+
};
364+
365+
const result = await configurationsController.updateSandboxConfig(requestContext);
366+
const error = await result.json();
367+
368+
expect(result.status).to.equal(404);
369+
expect(error.message).to.include('Configuration not found');
370+
});
371+
372+
it('should return bad request when Configuration.create throws an error', async () => {
373+
const requestContext = {
374+
data: {
375+
sandboxConfigs: {
376+
cwv: { expire: '10' },
377+
},
378+
},
379+
};
380+
381+
mockDataAccess.Configuration.create.throws(new Error('Create failed'));
382+
383+
const result = await configurationsController.updateSandboxConfig(requestContext);
384+
const error = await result.json();
385+
386+
expect(result.status).to.equal(400);
387+
expect(error.message).to.include('Error updating sandbox configuration: Create failed');
388+
});
389+
390+
it('should handle when getHandlers returns null', async () => {
391+
const requestContext = {
392+
data: {
393+
sandboxConfigs: {
394+
cwv: { expire: '10' },
395+
},
396+
},
397+
};
398+
399+
// Mock getHandlers to return null to test the || {} fallback
400+
mockCurrentConfig.getHandlers.returns(null);
401+
402+
const result = await configurationsController.updateSandboxConfig(requestContext);
403+
const response = await result.json();
404+
405+
expect(result.status).to.equal(200);
406+
expect(response).to.have.property('message', 'Sandbox configurations updated successfully');
407+
expect(mockDataAccess.Configuration.create).to.have.been.calledWith({
408+
jobs: [],
409+
handlers: {
410+
cwv: { sandbox: { expire: '10' } },
411+
},
412+
queues: {},
413+
slackRoles: {},
414+
});
415+
});
416+
417+
it('should handle when audit type does not exist in handlers', async () => {
418+
const requestContext = {
419+
data: {
420+
sandboxConfigs: {
421+
newAuditType: { enabled: true },
422+
},
423+
},
424+
};
425+
426+
// Mock getHandlers to return handlers without the new audit type
427+
mockCurrentConfig.getHandlers.returns({
428+
cwv: { sandbox: { expire: '5' } },
429+
});
430+
431+
const result = await configurationsController.updateSandboxConfig(requestContext);
432+
const response = await result.json();
433+
434+
expect(result.status).to.equal(200);
435+
expect(response).to.have.property('message', 'Sandbox configurations updated successfully');
436+
expect(mockDataAccess.Configuration.create).to.have.been.calledWith({
437+
jobs: [],
438+
handlers: {
439+
cwv: { sandbox: { expire: '5' } },
440+
newAuditType: { sandbox: { enabled: true } },
441+
},
442+
queues: {},
443+
slackRoles: {},
444+
});
445+
});
446+
447+
it('should handle when audit type exists but has no sandbox property', async () => {
448+
const requestContext = {
449+
data: {
450+
sandboxConfigs: {
451+
lhs: { timeout: '30' },
452+
},
453+
},
454+
};
455+
456+
// Mock getHandlers to return handlers with audit type but no sandbox property
457+
mockCurrentConfig.getHandlers.returns({
458+
cwv: { sandbox: { expire: '5' } },
459+
lhs: { enabled: true }, // No sandbox property
460+
});
461+
462+
const result = await configurationsController.updateSandboxConfig(requestContext);
463+
const response = await result.json();
464+
465+
expect(result.status).to.equal(200);
466+
expect(response).to.have.property('message', 'Sandbox configurations updated successfully');
467+
expect(mockDataAccess.Configuration.create).to.have.been.calledWith({
468+
jobs: [],
469+
handlers: {
470+
cwv: { sandbox: { expire: '5' } },
471+
lhs: { enabled: true, sandbox: { timeout: '30' } },
472+
},
473+
queues: {},
474+
slackRoles: {},
475+
});
476+
});
477+
478+
it('should handle when getHandlers returns undefined', async () => {
479+
const requestContext = {
480+
data: {
481+
sandboxConfigs: {
482+
cwv: { expire: '10' },
483+
},
484+
},
485+
};
486+
487+
// Mock getHandlers to return undefined to test the || {} fallback
488+
mockCurrentConfig.getHandlers.returns(undefined);
489+
490+
const result = await configurationsController.updateSandboxConfig(requestContext);
491+
const response = await result.json();
492+
493+
expect(result.status).to.equal(200);
494+
expect(response).to.have.property('message', 'Sandbox configurations updated successfully');
495+
expect(mockDataAccess.Configuration.create).to.have.been.calledWith({
496+
jobs: [],
497+
handlers: {
498+
cwv: { sandbox: { expire: '10' } },
499+
},
500+
queues: {},
501+
slackRoles: {},
502+
});
503+
});
504+
505+
it('should handle when audit type does not exist in current handlers', async () => {
506+
const requestContext = {
507+
data: {
508+
sandboxConfigs: {
509+
newAuditType: { timeout: '15' },
510+
},
511+
},
512+
};
513+
514+
// Mock getHandlers to return handlers without the new audit type
515+
mockCurrentConfig.getHandlers.returns({
516+
cwv: { sandbox: { expire: '5' } },
517+
// newAuditType doesn't exist, so lines 120-122 will be executed
518+
});
519+
520+
const result = await configurationsController.updateSandboxConfig(requestContext);
521+
const response = await result.json();
522+
523+
expect(result.status).to.equal(200);
524+
expect(response).to.have.property('message', 'Sandbox configurations updated successfully');
525+
expect(mockDataAccess.Configuration.create).to.have.been.calledWith({
526+
jobs: [],
527+
handlers: {
528+
cwv: { sandbox: { expire: '5' } },
529+
newAuditType: { sandbox: { timeout: '15' } }, // New audit type created
530+
},
531+
queues: {},
532+
slackRoles: {},
533+
});
534+
});
535+
});
536+
});
238537
});

0 commit comments

Comments
 (0)