diff --git a/src/controllers/llmo/llmo.js b/src/controllers/llmo/llmo.js index 619d9d9e9..0f19af103 100644 --- a/src/controllers/llmo/llmo.js +++ b/src/controllers/llmo/llmo.js @@ -833,6 +833,38 @@ function LlmoController(ctx) { } }; + // Handles requests to update per-site country code ignore list for CDN logs reports + const patchLlmoCountryCodeIgnoreList = async (context) => { + const { log } = context; + const { data } = context; + const { siteId } = context.params; + + try { + if (!accessControlUtil.isLLMOAdministrator()) { + return forbidden('Only LLMO administrators can update the country code ignore list'); + } + + const siteValidation = await getSiteAndValidateLlmo(context); + if (siteValidation.status) return siteValidation; + const { site, config } = siteValidation; + + if (!isObject(data)) { + return badRequest('Update data must be provided as an object'); + } + + const { countryCodeIgnoreList } = data; + + config.updateLlmoCountryCodeIgnoreList(countryCodeIgnoreList); + + await saveSiteConfig(site, config, log, 'updating country code ignore list'); + + return ok(config.getLlmoConfig().countryCodeIgnoreList || []); + } catch (error) { + log.error(`Error updating country code ignore list for siteId: ${siteId}, error: ${error.message}`); + return badRequest(error.message); + } + }; + /** * Onboards a new customer to LLMO. * This endpoint handles the complete onboarding process for net new customers @@ -1702,6 +1734,7 @@ function LlmoController(ctx) { patchLlmoCustomerIntent, patchLlmoCdnLogsFilter, patchLlmoCdnBucketConfig, + patchLlmoCountryCodeIgnoreList, updateLlmoConfig, onboardCustomer, offboardCustomer, diff --git a/src/routes/index.js b/src/routes/index.js index bf9af23f3..cca80d1a6 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -372,6 +372,7 @@ export default function getRouteHandlers( 'PATCH /sites/:siteId/llmo/customer-intent/:intentKey': llmoController.patchLlmoCustomerIntent, 'PATCH /sites/:siteId/llmo/cdn-logs-filter': llmoController.patchLlmoCdnLogsFilter, 'PATCH /sites/:siteId/llmo/cdn-logs-bucket-config': llmoController.patchLlmoCdnBucketConfig, + 'PATCH /sites/:siteId/llmo/country-code-ignore-list': llmoController.patchLlmoCountryCodeIgnoreList, 'GET /sites/:siteId/llmo/global-sheet-data/:configName': llmoController.getLlmoGlobalSheetData, 'GET /sites/:siteId/llmo/rationale': llmoController.getLlmoRationale, 'GET /sites/:siteId/llmo/brand-claims': llmoController.getBrandClaims, diff --git a/test/controllers/llmo/llmo.test.js b/test/controllers/llmo/llmo.test.js index 7241856b5..5b9a8f209 100644 --- a/test/controllers/llmo/llmo.test.js +++ b/test/controllers/llmo/llmo.test.js @@ -330,6 +330,7 @@ describe('LlmoController', () => { updateLlmoCustomerIntent: sinon.stub(), updateLlmoCdnlogsFilter: sinon.stub(), updateLlmoCdnBucketConfig: sinon.stub(), + updateLlmoCountryCodeIgnoreList: sinon.stub(), addLlmoTag: sinon.stub(), state: { llmo: mockLlmoConfig }, getSlackConfig: sinon.stub().returns(null), @@ -2459,6 +2460,77 @@ describe('LlmoController', () => { }); }); + describe('patchLlmoCountryCodeIgnoreList', () => { + beforeEach(() => { + mockContext.data = { + countryCodeIgnoreList: ['PS', 'AD'], + }; + }); + + it('should update country code ignore list successfully', async () => { + const result = await controller.patchLlmoCountryCodeIgnoreList(mockContext); + + expect(result.status).to.equal(200); + expect(mockConfig.updateLlmoCountryCodeIgnoreList).to.have.been.calledWith( + mockContext.data.countryCodeIgnoreList, + ); + }); + + it('should return bad request when no data provided', async () => { + mockContext.data = null; + + const result = await controller.patchLlmoCountryCodeIgnoreList(mockContext); + + expect(result.status).to.equal(400); + }); + + it('should handle errors and log them', async () => { + mockDataAccess.Site.findById.rejects(new Error('Database error')); + + const result = await controller.patchLlmoCountryCodeIgnoreList(mockContext); + + expect(result.status).to.equal(400); + expect(mockLog.error).to.have.been.calledWith( + `Error updating country code ignore list for siteId: ${TEST_SITE_ID}, error: Database error`, + ); + }); + + it('should return empty array when getLlmoConfig().countryCodeIgnoreList is null', async () => { + mockConfig.getLlmoConfig.returns({ + dataFolder: TEST_FOLDER, + brand: TEST_BRAND, + countryCodeIgnoreList: null, + }); + + const result = await controller.patchLlmoCountryCodeIgnoreList(mockContext); + + expect(result.status).to.equal(200); + const body = await result.json(); + expect(body).to.be.an('array'); + }); + + it('should return 403 when user is not LLMO administrator', async () => { + const LlmoControllerNoAdmin = await esmock('../../../src/controllers/llmo/llmo.js', { + '../../../src/support/access-control-util.js': createMockAccessControlUtil(true, true, false), + '@adobe/spacecat-shared-http-utils': mockHttpUtils, + ...getCommonMocks(), + }); + + const controllerNoAdmin = LlmoControllerNoAdmin(mockContext); + const result = await controllerNoAdmin.patchLlmoCountryCodeIgnoreList(mockContext); + + expect(result.status).to.equal(403); + const responseBody = await result.json(); + expect(responseBody.message).to.equal('Only LLMO administrators can update the country code ignore list'); + }); + + it('should return 404 when site is not found', async () => { + mockDataAccess.Site.findById.resolves(null); + const result = await controller.patchLlmoCountryCodeIgnoreList(mockContext); + expect(result.status).to.equal(404); + }); + }); + describe('onboardCustomer', () => { let mockOrganization; let mockNewSite; diff --git a/test/routes/index.test.js b/test/routes/index.test.js index c3b278374..5acae6898 100755 --- a/test/routes/index.test.js +++ b/test/routes/index.test.js @@ -271,6 +271,7 @@ describe('getRouteHandlers', () => { patchLlmoCustomerIntent: () => null, patchLlmoCdnLogsFilter: () => null, patchLlmoCdnBucketConfig: () => null, + patchLlmoCountryCodeIgnoreList: () => null, onboardCustomer: () => null, offboardCustomer: () => null, queryFiles: () => null, @@ -683,6 +684,7 @@ describe('getRouteHandlers', () => { 'PATCH /sites/:siteId/llmo/cdn-logs-filter', 'POST /sites/:siteId/sandbox/audit', 'PATCH /sites/:siteId/llmo/cdn-logs-bucket-config', + 'PATCH /sites/:siteId/llmo/country-code-ignore-list', 'GET /sites/:siteId/llmo/global-sheet-data/:configName', 'GET /sites/:siteId/llmo/rationale', 'GET /sites/:siteId/llmo/brand-claims', @@ -930,6 +932,8 @@ describe('getRouteHandlers', () => { expect(dynamicRoutes['GET /sites/:siteId/traffic/paid/url-page-type'].paramNames).to.deep.equal(['siteId']); expect(dynamicRoutes['PATCH /sites/:siteId/llmo/cdn-logs-bucket-config'].handler).to.equal(mockLlmoController.patchLlmoCdnBucketConfig); expect(dynamicRoutes['PATCH /sites/:siteId/llmo/cdn-logs-bucket-config'].paramNames).to.deep.equal(['siteId']); + expect(dynamicRoutes['PATCH /sites/:siteId/llmo/country-code-ignore-list'].handler).to.equal(mockLlmoController.patchLlmoCountryCodeIgnoreList); + expect(dynamicRoutes['PATCH /sites/:siteId/llmo/country-code-ignore-list'].paramNames).to.deep.equal(['siteId']); expect(dynamicRoutes['GET /sites/:siteId/llmo/global-sheet-data/:configName'].handler).to.equal(mockLlmoController.getLlmoGlobalSheetData); expect(dynamicRoutes['GET /sites/:siteId/llmo/global-sheet-data/:configName'].paramNames).to.deep.equal(['siteId', 'configName']); expect(dynamicRoutes['GET /sites/:siteId/llmo/rationale'].handler).to.equal(mockLlmoController.getLlmoRationale);