Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/support/slack/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -94,4 +95,5 @@ export default (context) => [
runA11yCodefix(context),
identifyRedirects(context),
identifyAndUpdateRedirects(context),
detectCdn(context),
];
94 changes: 94 additions & 0 deletions src/support/slack/commands/detect-cdn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* 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 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
* 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,
usageText: `${PHRASES[0]} {url}`,
handleExecution,
};
}
206 changes: 206 additions & 0 deletions test/support/slack/commands/detect-cdn.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/*
* 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 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
* 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().callsFake(async (sayFn, err) => {
await sayFn(`:nuclear-warning: Oops! Something went wrong: ${err.message}`);
});

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);
});
});
Loading