From 50ca3245727107881d5d27495f3a8161afc65b41 Mon Sep 17 00:00:00 2001 From: Jesus Sanchez <31286739+jsanchez07@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:32:36 -0500 Subject: [PATCH 1/4] Adding a slack command for detecting CDN for customers --- src/support/slack/commands.js | 2 + src/support/slack/commands/detect-cdn.js | 93 ++++++++ .../support/slack/commands/detect-cdn.test.js | 204 ++++++++++++++++++ 3 files changed, 299 insertions(+) create mode 100644 src/support/slack/commands/detect-cdn.js create mode 100644 test/support/slack/commands/detect-cdn.test.js diff --git a/src/support/slack/commands.js b/src/support/slack/commands.js index 5a93ae8e9..a811b76b4 100644 --- a/src/support/slack/commands.js +++ b/src/support/slack/commands.js @@ -48,6 +48,7 @@ import runPageCitability from './commands/run-page-citability.js'; import runA11yCodefix from './commands/run-a11y-codefix.js'; import identifyRedirects from './commands/identify-redirects.js'; import identifyAndUpdateRedirects from './commands/identify-and-update-redirects.js'; +import detectCdn from './commands/detect-cdn.js'; /** * Returns all commands. @@ -94,4 +95,5 @@ export default (context) => [ runA11yCodefix(context), identifyRedirects(context), identifyAndUpdateRedirects(context), + detectCdn(context), ]; diff --git a/src/support/slack/commands/detect-cdn.js b/src/support/slack/commands/detect-cdn.js new file mode 100644 index 000000000..e135d475c --- /dev/null +++ b/src/support/slack/commands/detect-cdn.js @@ -0,0 +1,93 @@ +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use it except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { hasText } from '@adobe/spacecat-shared-utils'; + +import BaseCommand from './base.js'; +import { extractURLFromSlackInput, postErrorMessage } from '../../../utils/slack/base.js'; + +const PHRASES = ['detect-cdn']; + +export default function DetectCdnCommand(context) { + const baseCommand = BaseCommand({ + id: 'detect-cdn', + name: 'Detect CDN', + description: 'Detects which CDN a website uses (e.g. Cloudflare, Akamai, Fastly) from HTTP headers. Optional: pass a Spacecat site base URL to associate the result with a site for future onboarding.', + phrases: PHRASES, + usageText: `${PHRASES[0]} {url}`, + }); + + const { + dataAccess, + env, + log, + sqs, + } = context; + const { Site } = dataAccess; + + const handleExecution = async (args, slackContext) => { + const { say, channelId, threadTs } = slackContext; + + try { + const [urlInput] = args; + const baseURL = extractURLFromSlackInput(urlInput); + + if (!baseURL) { + await say(baseCommand.usage()); + return; + } + + if (!hasText(env?.AUDIT_JOBS_QUEUE_URL)) { + await say(':x: Server misconfiguration: missing `AUDIT_JOBS_QUEUE_URL`.'); + return; + } + + if (!sqs) { + await say(':x: Server misconfiguration: missing SQS client.'); + return; + } + + // Optional: if URL matches a Spacecat site, pass siteId for future onboarding integration + let siteId = null; + try { + const site = await Site.findByBaseURL(baseURL); + if (site) { + siteId = site.getId(); + } + } catch { + // ignore; we can still detect CDN for any URL + } + + await say(`:mag: Queued CDN detection for *${baseURL}*. I'll reply here when it's ready.`); + + await sqs.sendMessage(env.AUDIT_JOBS_QUEUE_URL, { + type: 'detect-cdn', + baseURL, + ...(siteId && { siteId }), + slackContext: { + channelId, + threadTs, + }, + }); + } catch (error) { + log.error(error); + await postErrorMessage(say, error); + } + }; + + baseCommand.init(context); + + return { + ...baseCommand, + handleExecution, + }; +} diff --git a/test/support/slack/commands/detect-cdn.test.js b/test/support/slack/commands/detect-cdn.test.js new file mode 100644 index 000000000..f01a3a302 --- /dev/null +++ b/test/support/slack/commands/detect-cdn.test.js @@ -0,0 +1,204 @@ +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use it except in compliance with the License. You may obtain a copy + * of the License at https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect, use } from 'chai'; +import sinonChai from 'sinon-chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; + +use(sinonChai); + +describe('DetectCdnCommand', () => { + let DetectCdnCommand; + let context; + let slackContext; + let dataAccessStub; + let sqsStub; + let extractURLFromSlackInputStub; + let postErrorMessageStub; + + beforeEach(async function beforeEachHook() { + this.timeout(10000); + extractURLFromSlackInputStub = sinon.stub(); + postErrorMessageStub = sinon.stub().resolves(); + + DetectCdnCommand = (await esmock( + '../../../../src/support/slack/commands/detect-cdn.js', + { + '../../../../src/utils/slack/base.js': { + extractURLFromSlackInput: extractURLFromSlackInputStub, + postErrorMessage: postErrorMessageStub, + }, + }, + )).default; + + dataAccessStub = { + Site: { + findByBaseURL: sinon.stub(), + }, + }; + + sqsStub = { + sendMessage: sinon.stub().resolves(), + }; + + context = { + dataAccess: dataAccessStub, + env: { AUDIT_JOBS_QUEUE_URL: 'https://sqs.example.com/queue' }, + log: { + error: sinon.spy(), + }, + sqs: sqsStub, + }; + + slackContext = { + say: sinon.stub().resolves(), + channelId: 'C123', + threadTs: '1712345678.9012', + }; + }); + + it('initializes with base command metadata', () => { + const command = DetectCdnCommand(context); + expect(command.id).to.equal('detect-cdn'); + expect(command.name).to.equal('Detect CDN'); + expect(command.phrases).to.deep.equal(['detect-cdn']); + expect(command.usageText).to.equal('detect-cdn {url}'); + }); + + it('shows usage when URL is missing/invalid', async () => { + extractURLFromSlackInputStub.returns(null); + const command = DetectCdnCommand(context); + + await command.handleExecution([], slackContext); + + expect(slackContext.say).to.have.been.calledOnceWith(command.usage()); + }); + + it('notifies when AUDIT_JOBS_QUEUE_URL is missing', async () => { + extractURLFromSlackInputStub.returns('https://example.com'); + const command = DetectCdnCommand({ + ...context, + env: {}, + }); + + await command.handleExecution(['https://example.com'], slackContext); + + expect(slackContext.say).to.have.been.calledOnce; + expect(slackContext.say.firstCall.args[0]).to.include('missing `AUDIT_JOBS_QUEUE_URL`'); + }); + + it('notifies when env is undefined', async () => { + extractURLFromSlackInputStub.returns('https://example.com'); + const { env, ...contextWithoutEnv } = context; + const command = DetectCdnCommand(contextWithoutEnv); + + await command.handleExecution(['https://example.com'], slackContext); + + expect(slackContext.say).to.have.been.calledOnce; + expect(slackContext.say.firstCall.args[0]).to.include('missing `AUDIT_JOBS_QUEUE_URL`'); + }); + + it('notifies when SQS client is missing', async () => { + extractURLFromSlackInputStub.returns('https://example.com'); + const command = DetectCdnCommand({ + ...context, + sqs: null, + }); + + await command.handleExecution(['https://example.com'], slackContext); + + expect(slackContext.say).to.have.been.calledOnce; + expect(slackContext.say.firstCall.args[0]).to.include('missing SQS client'); + }); + + it('says queued and sends SQS message when URL is valid and no site found', async () => { + extractURLFromSlackInputStub.returns('https://example.com'); + dataAccessStub.Site.findByBaseURL.resolves(null); + const command = DetectCdnCommand(context); + + await command.handleExecution(['https://example.com'], slackContext); + + expect(slackContext.say).to.have.been.calledOnce; + expect(slackContext.say.firstCall.args[0]).to.include('Queued CDN detection for *https://example.com*'); + expect(sqsStub.sendMessage).to.have.been.calledOnce; + expect(sqsStub.sendMessage.firstCall.args[0]).to.equal('https://sqs.example.com/queue'); + expect(sqsStub.sendMessage.firstCall.args[1]).to.deep.equal({ + type: 'detect-cdn', + baseURL: 'https://example.com', + slackContext: { + channelId: 'C123', + threadTs: '1712345678.9012', + }, + }); + }); + + it('includes siteId in SQS payload when site is found for base URL', async () => { + extractURLFromSlackInputStub.returns('https://mysite.com'); + dataAccessStub.Site.findByBaseURL.resolves({ + getId: () => 'site-uuid-123', + }); + const command = DetectCdnCommand(context); + + await command.handleExecution(['https://mysite.com'], slackContext); + + expect(slackContext.say).to.have.been.calledOnce; + expect(sqsStub.sendMessage).to.have.been.calledOnce; + expect(sqsStub.sendMessage.firstCall.args[1]).to.deep.include({ + type: 'detect-cdn', + baseURL: 'https://mysite.com', + siteId: 'site-uuid-123', + slackContext: { + channelId: 'C123', + threadTs: '1712345678.9012', + }, + }); + }); + + it('still queues job without siteId when Site.findByBaseURL throws', async () => { + extractURLFromSlackInputStub.returns('https://example.com'); + dataAccessStub.Site.findByBaseURL.rejects(new Error('db error')); + const command = DetectCdnCommand(context); + + await command.handleExecution(['https://example.com'], slackContext); + + expect(slackContext.say).to.have.been.calledOnce; + expect(slackContext.say.firstCall.args[0]).to.include('Queued CDN detection'); + expect(sqsStub.sendMessage).to.have.been.calledOnce; + expect(sqsStub.sendMessage.firstCall.args[1]).to.not.have.property('siteId'); + expect(sqsStub.sendMessage.firstCall.args[1]).to.deep.include({ + type: 'detect-cdn', + baseURL: 'https://example.com', + slackContext: { + channelId: 'C123', + threadTs: '1712345678.9012', + }, + }); + }); + + it('logs and posts error when an exception occurs in handleExecution', async () => { + extractURLFromSlackInputStub.returns('https://example.com'); + dataAccessStub.Site.findByBaseURL.resolves(null); + sqsStub.sendMessage.rejects(new Error('SQS failure')); + const command = DetectCdnCommand(context); + + await command.handleExecution(['https://example.com'], slackContext); + + expect(slackContext.say).to.have.been.calledTwice; // once "Queued...", then error + expect(context.log.error).to.have.been.calledOnce; + expect(postErrorMessageStub).to.have.been.calledOnce; + expect(postErrorMessageStub.firstCall.args[0]).to.equal(slackContext.say); + expect(postErrorMessageStub.firstCall.args[1]).to.be.instanceOf(Error); + }); +}); From bb745998e96de3e66f239056d264c6ec034203ee Mon Sep 17 00:00:00 2001 From: Jesus Sanchez <31286739+jsanchez07@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:16:45 -0500 Subject: [PATCH 2/4] fixing linter errors --- src/support/slack/commands/detect-cdn.js | 2 +- test/support/slack/commands/detect-cdn.test.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/support/slack/commands/detect-cdn.js b/src/support/slack/commands/detect-cdn.js index e135d475c..58b1092ee 100644 --- a/src/support/slack/commands/detect-cdn.js +++ b/src/support/slack/commands/detect-cdn.js @@ -1,7 +1,7 @@ /* * Copyright 2026 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use it except in compliance with the License. You may obtain a copy + * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under diff --git a/test/support/slack/commands/detect-cdn.test.js b/test/support/slack/commands/detect-cdn.test.js index f01a3a302..c01cabfde 100644 --- a/test/support/slack/commands/detect-cdn.test.js +++ b/test/support/slack/commands/detect-cdn.test.js @@ -1,8 +1,8 @@ /* * Copyright 2026 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use it except in compliance with the License. You may obtain a copy - * of the License at https://www.apache.org/licenses/LICENSE-2.0 + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS @@ -101,7 +101,7 @@ describe('DetectCdnCommand', () => { it('notifies when env is undefined', async () => { extractURLFromSlackInputStub.returns('https://example.com'); - const { env, ...contextWithoutEnv } = context; + const { env: _env, ...contextWithoutEnv } = context; const command = DetectCdnCommand(contextWithoutEnv); await command.handleExecution(['https://example.com'], slackContext); From 4063c3a752b7194f98d9d9268702f6c64b7c92dc Mon Sep 17 00:00:00 2001 From: Jesus Sanchez <31286739+jsanchez07@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:21:12 -0500 Subject: [PATCH 3/4] fixing more linter errors --- test/support/slack/commands/detect-cdn.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/support/slack/commands/detect-cdn.test.js b/test/support/slack/commands/detect-cdn.test.js index c01cabfde..4e4fc5454 100644 --- a/test/support/slack/commands/detect-cdn.test.js +++ b/test/support/slack/commands/detect-cdn.test.js @@ -101,7 +101,7 @@ describe('DetectCdnCommand', () => { it('notifies when env is undefined', async () => { extractURLFromSlackInputStub.returns('https://example.com'); - const { env: _env, ...contextWithoutEnv } = context; + const { env: _, ...contextWithoutEnv } = context; const command = DetectCdnCommand(contextWithoutEnv); await command.handleExecution(['https://example.com'], slackContext); From 3520b6b17c8a1c6249b5cf7938c99341787d67c9 Mon Sep 17 00:00:00 2001 From: Jesus Sanchez <31286739+jsanchez07@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:29:43 -0500 Subject: [PATCH 4/4] fixing test case coverage --- src/support/slack/commands/detect-cdn.js | 1 + test/support/slack/commands/detect-cdn.test.js | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/support/slack/commands/detect-cdn.js b/src/support/slack/commands/detect-cdn.js index 58b1092ee..e7baea39b 100644 --- a/src/support/slack/commands/detect-cdn.js +++ b/src/support/slack/commands/detect-cdn.js @@ -88,6 +88,7 @@ export default function DetectCdnCommand(context) { return { ...baseCommand, + usageText: `${PHRASES[0]} {url}`, handleExecution, }; } diff --git a/test/support/slack/commands/detect-cdn.test.js b/test/support/slack/commands/detect-cdn.test.js index 4e4fc5454..59c3c3efc 100644 --- a/test/support/slack/commands/detect-cdn.test.js +++ b/test/support/slack/commands/detect-cdn.test.js @@ -31,7 +31,9 @@ describe('DetectCdnCommand', () => { beforeEach(async function beforeEachHook() { this.timeout(10000); extractURLFromSlackInputStub = sinon.stub(); - postErrorMessageStub = sinon.stub().resolves(); + postErrorMessageStub = sinon.stub().callsFake(async (sayFn, err) => { + await sayFn(`:nuclear-warning: Oops! Something went wrong: ${err.message}`); + }); DetectCdnCommand = (await esmock( '../../../../src/support/slack/commands/detect-cdn.js',