Skip to content

Commit d07c146

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 d07c146

File tree

6 files changed

+541
-4159
lines changed

6 files changed

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

test/routes/index.test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ describe('getRouteHandlers', () => {
2929
getByVersion: sinon.stub(),
3030
getLatest: sinon.stub(),
3131
updateConfiguration: sinon.stub(),
32+
updateSandboxConfig: sinon.stub(),
3233
};
3334

3435
const mockHooksController = {
@@ -231,6 +232,7 @@ describe('getRouteHandlers', () => {
231232
'GET /configurations',
232233
'GET /configurations/latest',
233234
'PUT /configurations/latest',
235+
'PATCH /configurations/sandbox',
234236
'PATCH /configurations/sites/audits',
235237
'GET /organizations',
236238
'POST /organizations',
@@ -256,6 +258,7 @@ describe('getRouteHandlers', () => {
256258
expect(staticRoutes['GET /configurations']).to.equal(mockConfigurationController.getAll);
257259
expect(staticRoutes['GET /configurations/latest']).to.equal(mockConfigurationController.getLatest);
258260
expect(staticRoutes['PUT /configurations/latest']).to.equal(mockConfigurationController.updateConfiguration);
261+
expect(staticRoutes['PATCH /configurations/sandbox']).to.equal(mockConfigurationController.updateSandboxConfig);
259262
expect(staticRoutes['PATCH /configurations/sites/audits']).to.equal(mockSitesAuditsToggleController.execute);
260263
expect(staticRoutes['GET /organizations']).to.equal(mockOrganizationsController.getAll);
261264
expect(staticRoutes['POST /organizations']).to.equal(mockOrganizationsController.createOrganization);

0 commit comments

Comments
 (0)