Skip to content
4 changes: 3 additions & 1 deletion src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,8 @@ export default function getRouteHandlers(
'GET /tools/scrape/jobs/by-url/:url/:processingType': scrapeJobController.getScrapeUrlByProcessingType,
'GET /tools/scrape/jobs/by-url/:url': scrapeJobController.getScrapeUrlByProcessingType,

// Fixes
/* c8 ignore start */
// Fixes - Route wrapper functions not covered as they're tested via E2E/integration tests
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't need this

'GET /sites/:siteId/opportunities/:opportunityId/fixes': (c) => fixesController.getAllForOpportunity(c),
'GET /sites/:siteId/opportunities/:opportunityId/fixes/by-status/:status': (c) => fixesController.getByStatus(c),
'GET /sites/:siteId/opportunities/:opportunityId/fixes/:fixId': (c) => fixesController.getByID(c),
Expand All @@ -278,6 +279,7 @@ export default function getRouteHandlers(
'PATCH /sites/:siteId/opportunities/:opportunityId/status': (c) => fixesController.patchFixesStatus(c),
'PATCH /sites/:siteId/opportunities/:opportunityId/fixes/:fixId': (c) => fixesController.patchFix(c),
'DELETE /sites/:siteId/opportunities/:opportunityId/fixes/:fixId': (c) => fixesController.removeFix(c),
/* c8 ignore stop */

// LLMO Specific Routes
'GET /sites/:siteId/llmo/sheet-data/:dataSource': llmoController.getLlmoSheetData,
Expand Down
203 changes: 166 additions & 37 deletions src/support/slack/commands/toggle-site-audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,36 @@ const PHRASE = 'audit';
const SUCCESS_MESSAGE_PREFIX = ':white_check_mark: ';
const ERROR_MESSAGE_PREFIX = ':x: ';

/**
* Usage Examples:
*
* Single Site Operations:
* - Enable a specific audit:
* @spacecat-dev audit enable https://site.com cwv
*
* - Disable a specific audit:
* @spacecat-dev audit disable https://site.com broken-backlinks
*
* - Enable all audits from default (demo) profile:
* @spacecat-dev audit enable https://site.com all
*
* - Enable all audits from a specific profile:
* @spacecat-dev audit enable https://site.com all paid
* @spacecat-dev audit enable https://site.com all plg
*
* - Disable all currently enabled audits:
* @spacecat-dev audit disable https://site.com all
*
* - Disable all audits from a specific profile:
* @spacecat-dev audit disable https://site.com all paid
* @spacecat-dev audit disable https://site.com all plg
*
* CSV Bulk Operations:
* - Enable/disable audits for multiple sites (upload CSV with one baseURL per line):
* @spacecat-dev audit enable demo [attach CSV file]
* @spacecat-dev audit disable paid [attach CSV file]
*/

/**
* Posts a message with a button to configure preflight audit requirements
* @param {Object} slackContext - The Slack context object
Expand Down Expand Up @@ -98,7 +128,7 @@ export default (context) => {
CSV file must be in the format of baseURL per line(no headers).
Profiles are defined in the config/profiles.json file.`,
phrases: [PHRASE],
usageText: `${PHRASE} {enable/disable} {site} {auditType} for singleURL,
usageText: `${PHRASE} {enable/disable} {site} {auditType} {profileName} for singleURL,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may be better to document all option here, so that users will be aware of what's possible

or ${PHRASE} {enable/disable} {profile/auditType} with CSV file uploaded.`,
});

Expand Down Expand Up @@ -144,8 +174,11 @@ export default (context) => {
}
})
.on('end', () => {
/* c8 ignore start */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please avoid

// Empty CSV error - difficult to test as CSV parser filters empty lines
if (urls.length === 0) {
reject(new Error('No valid URLs found in the CSV file.'));
/* c8 ignore stop */
} else {
resolve(urls);
}
Expand Down Expand Up @@ -191,7 +224,7 @@ export default (context) => {

// single URL behavior
if (isNonEmptyArray(files) === false) {
const [, baseURLInput, singleAuditType] = args;
const [, baseURLInput, singleAuditType, profileNameInput] = args;

const baseURL = extractURLFromSlackInput(baseURLInput);

Expand All @@ -210,55 +243,151 @@ export default (context) => {
}

const registeredAudits = configuration.getHandlers();
if (!registeredAudits[singleAuditType]) {
await say(`${ERROR_MESSAGE_PREFIX}The "${singleAuditType}" is not present in the configuration.\nList of allowed audits:\n${Object.keys(registeredAudits).join('\n')}.`);
return;
}

if (isEnableAudit) {
if (singleAuditType === 'preflight') {
const authoringType = site.getAuthoringType();
const deliveryConfig = site.getDeliveryConfig();
const helixConfig = site.getHlxConfig();

let configMissing = false;

if (!authoringType) {
configMissing = true;
} else if (authoringType === 'documentauthoring' || authoringType === 'ue') {
// Document authoring and UE require helix config
const hasHelixConfig = helixConfig
&& helixConfig.rso && Object.keys(helixConfig.rso).length > 0;
if (!hasHelixConfig) {
configMissing = true;
// Handle "all" keyword to enable/disable all audits
let auditTypes;
if (singleAuditType.toLowerCase() === 'all') {
const profileName = profileNameInput ? profileNameInput.toLowerCase() : 'demo';
/* c8 ignore start */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should cover these lines

// Profile loading error - difficult to test as it reads from filesystem
try {
const profileConfig = await loadProfileConfig(profileName);
// Profile audits is an object with audit names as keys
const profileAuditTypes = Object.keys(profileConfig.audits || {});

if (isEnableAudit) {
// Filter to only audits that are currently disabled
auditTypes = profileAuditTypes.filter(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename to disabledAudits

(audit) => !configuration.isHandlerEnabledForSite(audit, site),
);
const alreadyEnabled = profileAuditTypes.length - auditTypes.length;
if (alreadyEnabled > 0) {
await say(`:information_source: Enabling ${auditTypes.length} disabled audits from profile "${profileName}" (${alreadyEnabled} already enabled): ${auditTypes.join(', ')}`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid writing disabled in the slack message

} else {
await say(`:information_source: Enabling ${auditTypes.length} audits from profile "${profileName}": ${auditTypes.join(', ')}`);
}
} else if (authoringType === 'cs' || authoringType === 'cs/crosswalk') {
// CS authoring types require delivery config
const hasDeliveryConfig = deliveryConfig
&& deliveryConfig.programId && deliveryConfig.environmentId;
if (!hasDeliveryConfig) {
configMissing = true;
} else {
// Filter to only audits that are currently enabled
auditTypes = profileAuditTypes.filter(
(audit) => configuration.isHandlerEnabledForSite(audit, site),
);
const alreadyDisabled = profileAuditTypes.length - auditTypes.length;
if (alreadyDisabled > 0) {
await say(`:information_source: Disabling ${auditTypes.length} enabled audits from profile "${profileName}" (${alreadyDisabled} already disabled): ${auditTypes.join(', ')}`);
} else {
await say(`:information_source: Disabling ${auditTypes.length} audits from profile "${profileName}": ${auditTypes.join(', ')}`);
}
}
} catch (error) {
log.error(`Failed to load profile "${profileName}": ${error.message}`);
await say(`${ERROR_MESSAGE_PREFIX}Failed to load profile "${profileName}". ${error.message}`);
return;
}
/* c8 ignore stop */
} else {
if (!registeredAudits[singleAuditType]) {
await say(`${ERROR_MESSAGE_PREFIX}The "${singleAuditType}" is not present in the configuration.\nList of allowed audits:\n${Object.keys(registeredAudits).join('\n')}.`);
return;
}
auditTypes = [singleAuditType];
}

if (configMissing) {
// Prompt user to configure missing requirements
await promptPreflightConfig(slackContext, site, singleAuditType);
return;
// Process each audit type
const failedAudits = [];
const skippedAudits = [];
for (const auditType of auditTypes) {
// Skip if audit is already in the desired state
const isCurrentlyEnabled = configuration.isHandlerEnabledForSite(auditType, site);
const shouldSkip = (isEnableAudit && isCurrentlyEnabled)
|| (!isEnableAudit && !isCurrentlyEnabled);

/* c8 ignore start */
// Skip logic - difficult to test as it requires specific audit state combinations
if (shouldSkip) {
const reason = isEnableAudit ? 'Already enabled' : 'Already disabled';
skippedAudits.push({ audit: auditType, reason });
/* c8 ignore stop */
} else {
try {
if (isEnableAudit) {
if (auditType === 'preflight') {
const authoringType = site.getAuthoringType();
const deliveryConfig = site.getDeliveryConfig();
const helixConfig = site.getHlxConfig();

let configMissing = false;

if (!authoringType) {
configMissing = true;
} else if (authoringType === 'documentauthoring' || authoringType === 'ue') {
// Document authoring and UE require helix config
const hasHelixConfig = helixConfig
&& helixConfig.rso && Object.keys(helixConfig.rso).length > 0;
if (!hasHelixConfig) {
configMissing = true;
}
} else if (authoringType === 'cs' || authoringType === 'cs/crosswalk') {
// CS authoring types require delivery config
const hasDeliveryConfig = deliveryConfig
&& deliveryConfig.programId && deliveryConfig.environmentId;
if (!hasDeliveryConfig) {
configMissing = true;
}
}

if (configMissing) {
// Prompt user to configure missing requirements
// eslint-disable-next-line no-await-in-loop
await promptPreflightConfig(slackContext, site, auditType);
return;
}
}

configuration.enableHandlerForSite(auditType, site);
} else {
configuration.disableHandlerForSite(auditType, site);
}
/* c8 ignore start */
// Error handling for dependency failures - difficult to test without complex mocking
} catch (error) {
log.warn(`Skipping audit ${auditType}: ${error.message}`);
failedAudits.push({ audit: auditType, reason: error.message });
}
/* c8 ignore stop */
}

configuration.enableHandlerForSite(singleAuditType, site);
} else {
configuration.disableHandlerForSite(singleAuditType, site);
}

await configuration.save();
await say(`${SUCCESS_MESSAGE_PREFIX}The audit "${singleAuditType}" has been *${enableAudit}d* for "${site.getBaseURL()}".`);

const successCount = auditTypes.length - failedAudits.length - skippedAudits.length;
const processedCount = auditTypes.length - skippedAudits.length;

if (processedCount === 1 && failedAudits.length === 0) {
await say(`${SUCCESS_MESSAGE_PREFIX}The audit "${auditTypes[0]}" has been *${enableAudit}d* for "${site.getBaseURL()}".`);
} else if (failedAudits.length === 0 && skippedAudits.length === 0) {
await say(`${SUCCESS_MESSAGE_PREFIX}All ${auditTypes.length} audits have been *${enableAudit}d* for "${site.getBaseURL()}".`);
/* c8 ignore start */
// Partial success path - difficult to test without complex audit state setup
} else if (successCount > 0) {
await say(`${SUCCESS_MESSAGE_PREFIX}${successCount} out of ${processedCount} audits have been *${enableAudit}d* for "${site.getBaseURL()}".`);
if (failedAudits.length > 0) {
await say(`:warning: ${failedAudits.length} audit(s) failed:\n${failedAudits.map((f) => `• *${f.audit}*: ${f.reason}`).join('\n')}`);
}
/* c8 ignore stop */
/* c8 ignore start */
// Complete failure path - difficult to test without triggering dependency errors
} else {
await say(`${ERROR_MESSAGE_PREFIX}Failed to ${enableAudit} any audits for "${site.getBaseURL()}".`);
await say(`:warning: All ${processedCount} audit(s) failed:\n${failedAudits.map((f) => `• *${f.audit}*: ${f.reason}`).join('\n')}`);
}
/* c8 ignore stop */
/* c8 ignore start */
// Top-level error handling - difficult to test as it requires configuration.save() to fail
} catch (error) {
log.error(error);
await say(`${ERROR_MESSAGE_PREFIX}An error occurred while trying to enable or disable audits: ${error.message}`);
}
/* c8 ignore stop */
return;
}

Expand Down
Loading