diff --git a/package-lock.json b/package-lock.json index 1424a3a7f..06da43ed9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -709,6 +709,7 @@ "resolved": "https://registry.npmjs.org/@adobe/helix-universal/-/helix-universal-5.3.0.tgz", "integrity": "sha512-1eKFpKZMNamJHhq6eFm9gMLhgQunsf34mEFbaqg9ChEXZYk18SYgUu5GeNTvzk5Rzo0h9AuSwLtnI2Up2OSiSA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@adobe/fetch": "4.2.3", "aws4": "1.13.2" @@ -4395,6 +4396,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.940.0.tgz", "integrity": "sha512-u2sXsNJazJbuHeWICvsj6RvNyJh3isedEfPvB21jK/kxcriK+dE/izlKC2cyxUjERCmku0zTFNzY9FhrLbYHjQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -9212,6 +9214,7 @@ "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.79.tgz", "integrity": "sha512-ZLAs5YMM5N2UXN3kExMglltJrKKoW7hs3KMZFlXUnD7a5DFKBYxPFMeXA4rT+uvTxuJRZPCYX0JKI5BhyAWx4A==", "license": "MIT", + "peer": true, "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", @@ -9438,6 +9441,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -9644,6 +9648,7 @@ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -9807,6 +9812,7 @@ "integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", @@ -11726,6 +11732,7 @@ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -12049,6 +12056,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -12095,6 +12103,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -12570,6 +12579,7 @@ "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.12.0.tgz", "integrity": "sha512-lwalRdxXRy+Sn49/vN7W507qqmBRk5Fy2o0a9U6XTjL9IV+oR5PUiiptoBrOcaYCiVuGld8OEbNqhm6wvV3m6A==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -13097,6 +13107,7 @@ "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -15306,6 +15317,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -19304,6 +19316,7 @@ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -20431,6 +20444,7 @@ "integrity": "sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/mobx" @@ -23454,6 +23468,7 @@ "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -24000,6 +24015,7 @@ "resolved": "https://registry.npmjs.org/openai/-/openai-5.12.2.tgz", "integrity": "sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==", "license": "Apache-2.0", + "peer": true, "bin": { "openai": "bin/cli" }, @@ -25117,6 +25133,7 @@ "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -25127,6 +25144,7 @@ "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -25836,6 +25854,7 @@ "integrity": "sha512-phCkJ6pjDi9ANdhuF5ElS10GGdAKY6R1Pvt9lT3SFhOwM4T7QZE7MLpBDbNruUx/Q3gFD92/UOFringGipRqZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.0-beta.1", "@semantic-release/error": "^4.0.0", @@ -26599,6 +26618,7 @@ "integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@sinonjs/commons": "^3.0.1", "@sinonjs/fake-timers": "^13.0.5", @@ -27164,6 +27184,7 @@ "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@emotion/is-prop-valid": "1.2.2", "@emotion/unitless": "0.8.1", @@ -28106,6 +28127,7 @@ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "license": "MIT", + "peer": true, "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", @@ -28849,6 +28871,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -29131,6 +29154,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -29140,6 +29164,7 @@ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", "license": "ISC", + "peer": true, "peerDependencies": { "zod": "^3.25 || ^4" } diff --git a/src/support/slack/commands/toggle-site-audit.js b/src/support/slack/commands/toggle-site-audit.js index acf1029e7..0334df681 100644 --- a/src/support/slack/commands/toggle-site-audit.js +++ b/src/support/slack/commands/toggle-site-audit.js @@ -100,8 +100,9 @@ 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, - or ${PHRASE} {enable/disable} {profile/auditType} with CSV file uploaded.`, + usageText: `${PHRASE} {enable/disable} {site} {auditType} for single URL, + or ${PHRASE} {enable/disable} {profile/auditType} with CSV file uploaded. + or ${PHRASE} disable {site} all [profile] to disable all audits from a profile.`, }); const { log, dataAccess } = context; @@ -193,11 +194,11 @@ export default (context) => { // single URL behavior if (isNonEmptyArray(files) === false) { - const [, baseURLInput, singleAuditType] = args; + const [, baseURLInput, auditType, profileName] = args; const baseURL = extractURLFromSlackInput(baseURLInput); - validateInput(enableAudit, singleAuditType); + validateInput(enableAudit, auditType); if (isValidUrl(baseURL) === false) { await say(`${ERROR_MESSAGE_PREFIX}Please provide either a CSV file or a single baseURL.`); @@ -212,13 +213,48 @@ 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')}.`); + + // Handle "all" keyword to disable all audits from a profile + if (auditType === 'all') { + if (isEnableAudit) { + await say(`${ERROR_MESSAGE_PREFIX}Enable all is not supported. Please enable audits individually or use a profile with CSV upload.`); + return; + } + + const targetProfile = profileName || 'demo'; + + let profile; + try { + profile = loadProfileConfig(targetProfile); + } catch (error) { + await say(`${ERROR_MESSAGE_PREFIX}Profile "${targetProfile}" not found.`); + return; + } + const profileAuditTypes = Object.keys(profile.audits || {}); + if (profileAuditTypes.length === 0) { + await say(`${ERROR_MESSAGE_PREFIX}No audits found in profile "${targetProfile}".`); + return; + } + + await say(`:hourglass_flowing_sand: Disabling all audits from profile "${targetProfile}" for ${site.getBaseURL()}...`); + + profileAuditTypes.forEach((type) => { + configuration.disableHandlerForSite(type, site); + }); + + await configuration.save(); + await say(`${SUCCESS_MESSAGE_PREFIX}Successfully disabled all audits from profile "${targetProfile}" for "${site.getBaseURL()}".\n\`\`\`${profileAuditTypes.join('\n')}\`\`\``); + return; + } + + // Handle single audit type + if (!registeredAudits[auditType]) { + await say(`${ERROR_MESSAGE_PREFIX}The "${auditType}" is not present in the configuration.\nList of allowed audits:\n${Object.keys(registeredAudits).join('\n')}.`); return; } if (isEnableAudit) { - if (singleAuditType === 'preflight') { + if (auditType === 'preflight') { const authoringType = site.getAuthoringType(); const deliveryConfig = site.getDeliveryConfig(); const helixConfig = site.getHlxConfig(); @@ -247,18 +283,18 @@ export default (context) => { if (configMissing) { // Prompt user to configure missing requirements - await promptPreflightConfig(slackContext, site, singleAuditType); + await promptPreflightConfig(slackContext, site, auditType); return; } } - configuration.enableHandlerForSite(singleAuditType, site); + configuration.enableHandlerForSite(auditType, site); } else { - configuration.disableHandlerForSite(singleAuditType, site); + configuration.disableHandlerForSite(auditType, site); } await configuration.save(); - await say(`${SUCCESS_MESSAGE_PREFIX}The audit "${singleAuditType}" has been *${enableAudit}d* for "${site.getBaseURL()}".`); + await say(`${SUCCESS_MESSAGE_PREFIX}The audit "${auditType}" has been *${enableAudit}d* for "${site.getBaseURL()}".`); } catch (error) { log.error(error); await say(`${ERROR_MESSAGE_PREFIX}An error occurred while trying to enable or disable audits: ${error.message}`); diff --git a/test/support/slack/commands/toggle-site-audit.test.js b/test/support/slack/commands/toggle-site-audit.test.js index ec6a2b4ef..1391caf8d 100644 --- a/test/support/slack/commands/toggle-site-audit.test.js +++ b/test/support/slack/commands/toggle-site-audit.test.js @@ -703,4 +703,130 @@ describe('UpdateSitesAuditsCommand', () => { expect(configurationMock.enableHandlerForSite.called).to.be.false; }); }); + + describe('Disable All Audits', () => { + let loadProfileConfigStub; + let ToggleSiteAuditCommandWithProfile; + + beforeEach(async () => { + loadProfileConfigStub = sinon.stub().returns({ + audits: { + cwv: {}, + 'meta-tags': {}, + 'broken-backlinks': {}, + }, + }); + + ToggleSiteAuditCommandWithProfile = await esmock('../../../../src/support/slack/commands/toggle-site-audit.js', { + '@adobe/spacecat-shared-utils': { + tracingFetch: fetchStub, + }, + '../../../../src/utils/slack/base.js': { + extractURLFromSlackInput: (input) => (input.startsWith('http') ? input : `https://${input}`), + loadProfileConfig: loadProfileConfigStub, + }, + }); + }); + + it('should disable all audits from demo profile by default', async () => { + dataAccessMock.Site.findByBaseURL.withArgs('https://site0.com').resolves(site); + + const command = ToggleSiteAuditCommandWithProfile(contextMock); + const args = ['disable', 'https://site0.com', 'all']; + await command.handleExecution(args, slackContextMock); + + expect(loadProfileConfigStub.calledWith('demo')).to.be.true; + expect(configurationMock.disableHandlerForSite.calledWith('cwv', site)).to.be.true; + expect(configurationMock.disableHandlerForSite.calledWith('meta-tags', site)).to.be.true; + expect(configurationMock.disableHandlerForSite.calledWith('broken-backlinks', site)).to.be.true; + expect(configurationMock.save.called).to.be.true; + expect(slackContextMock.say.calledWith(sinon.match(/Successfully disabled all audits/))).to.be.true; + }); + + it('should disable all audits from specified profile', async () => { + dataAccessMock.Site.findByBaseURL.withArgs('https://site0.com').resolves(site); + + const command = ToggleSiteAuditCommandWithProfile(contextMock); + const args = ['disable', 'https://site0.com', 'all', 'paid']; + await command.handleExecution(args, slackContextMock); + + expect(loadProfileConfigStub.calledWith('paid')).to.be.true; + expect(configurationMock.disableHandlerForSite.calledThrice).to.be.true; + expect(configurationMock.save.called).to.be.true; + expect(slackContextMock.say.calledWith(sinon.match(/from profile "paid"/))).to.be.true; + }); + + it('should reject enable all', async () => { + dataAccessMock.Site.findByBaseURL.withArgs('https://site0.com').resolves(site); + + const command = ToggleSiteAuditCommandWithProfile(contextMock); + const args = ['enable', 'https://site0.com', 'all']; + await command.handleExecution(args, slackContextMock); + + expect(configurationMock.enableHandlerForSite.called).to.be.false; + expect(configurationMock.save.called).to.be.false; + expect(slackContextMock.say.calledWith(sinon.match(/Enable all is not supported/))).to.be.true; + }); + + it('should show error if profile not found', async () => { + loadProfileConfigStub.throws(new Error('Profile not found')); + dataAccessMock.Site.findByBaseURL.withArgs('https://site0.com').resolves(site); + + const command = ToggleSiteAuditCommandWithProfile(contextMock); + const args = ['disable', 'https://site0.com', 'all', 'invalid']; + await command.handleExecution(args, slackContextMock); + + expect(configurationMock.disableHandlerForSite.called).to.be.false; + expect(configurationMock.save.called).to.be.false; + expect(slackContextMock.say.calledWith(sinon.match(/Profile "invalid" not found/))).to.be.true; + }); + + it('should show error if site not found for disable all', async () => { + dataAccessMock.Site.findByBaseURL.withArgs('https://site0.com').resolves(null); + + const command = ToggleSiteAuditCommandWithProfile(contextMock); + const args = ['disable', 'https://site0.com', 'all']; + await command.handleExecution(args, slackContextMock); + + expect(configurationMock.disableHandlerForSite.called).to.be.false; + expect(configurationMock.save.called).to.be.false; + expect(slackContextMock.say.calledWith(sinon.match(/site not found/))).to.be.true; + }); + + it('should handle profile with no audits gracefully', async () => { + loadProfileConfigStub.returns({ audits: {} }); // Empty audits object + dataAccessMock.Site.findByBaseURL.withArgs('https://site0.com').resolves(site); + + const command = ToggleSiteAuditCommandWithProfile(contextMock); + const args = ['disable', 'https://site0.com', 'all']; + await command.handleExecution(args, slackContextMock); + + expect(configurationMock.disableHandlerForSite.called).to.be.false; + expect(configurationMock.save.called).to.be.false; + }); + + it('should handle profile with null audits gracefully', async () => { + loadProfileConfigStub.returns({ audits: null }); // Null audits (tests || {} fallback) + dataAccessMock.Site.findByBaseURL.withArgs('https://site0.com').resolves(site); + + const command = ToggleSiteAuditCommandWithProfile(contextMock); + const args = ['disable', 'https://site0.com', 'all']; + await command.handleExecution(args, slackContextMock); + + expect(configurationMock.disableHandlerForSite.called).to.be.false; + expect(configurationMock.save.called).to.be.false; + }); + + it('should handle profile with undefined audits gracefully', async () => { + loadProfileConfigStub.returns({}); // No audits property (tests || {} fallback) + dataAccessMock.Site.findByBaseURL.withArgs('https://site0.com').resolves(site); + + const command = ToggleSiteAuditCommandWithProfile(contextMock); + const args = ['disable', 'https://site0.com', 'all']; + await command.handleExecution(args, slackContextMock); + + expect(configurationMock.disableHandlerForSite.called).to.be.false; + expect(configurationMock.save.called).to.be.false; + }); + }); });