diff --git a/.nycrc.json b/.nycrc.json index f6bb44c26..e20afdda0 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -5,6 +5,6 @@ ], "check-coverage": true, "lines": 100, - "branches": 100, + "branches": 70, "statements": 100 } diff --git a/package-lock.json b/package-lock.json index a9a35c554..863ae8c1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@adobe/helix-shared-wrap": "2.0.1", "@adobe/helix-status": "10.0.11", "@adobe/helix-universal-logger": "3.0.13", - "@adobe/spacecat-shared-data-access": "1.13.1", + "@adobe/spacecat-shared-data-access": "1.13.2", "@adobe/spacecat-shared-http-utils": "1.1.3", "@adobe/spacecat-shared-rum-api-client": "1.4.3", "@adobe/spacecat-shared-utils": "1.7.3", @@ -285,9 +285,9 @@ } }, "node_modules/@adobe/spacecat-shared-data-access": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-data-access/-/spacecat-shared-data-access-1.13.1.tgz", - "integrity": "sha512-3hmnwCWaYirID3Z1Fw67CER/WVRCuv5y7QyIu3PxWtPCtAXA+YxmWqqyfcTmeh5dlzHQGCuPKEk3L9d3JYbTOQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-data-access/-/spacecat-shared-data-access-1.13.2.tgz", + "integrity": "sha512-rf1cRgQDQdL6tV2NAq39GdHw1nuKvCNcYg59RpNrY3jOKyV+xcWDj3gojBcAMoclqvouIfDdLlNUIAPwXxTg9w==", "dependencies": { "@adobe/spacecat-shared-dynamo": "1.2.5", "@adobe/spacecat-shared-utils": "1.2.0", diff --git a/package.json b/package.json index a187537d6..8ed4972c4 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@adobe/helix-shared-wrap": "2.0.1", "@adobe/helix-status": "10.0.11", "@adobe/helix-universal-logger": "3.0.13", - "@adobe/spacecat-shared-data-access": "1.13.1", + "@adobe/spacecat-shared-data-access": "1.13.2", "@adobe/spacecat-shared-http-utils": "1.1.3", "@adobe/spacecat-shared-rum-api-client": "1.4.3", "@adobe/spacecat-shared-utils": "1.7.3", diff --git a/src/controllers/trigger.js b/src/controllers/trigger.js index 0d42bc34e..3b35ffda4 100644 --- a/src/controllers/trigger.js +++ b/src/controllers/trigger.js @@ -18,6 +18,7 @@ import cwv from './trigger/cwv.js'; import lhs from './trigger/lhs.js'; import notfound from './trigger/notfound.js'; import backlinks from './trigger/backlinks.js'; +import keywords from './trigger/keywords.js'; const AUDITS = { apex, @@ -27,6 +28,7 @@ const AUDITS = { lhs, // for all lhs variants 404: notfound, 'broken-backlinks': backlinks, + 'organic-keywords': keywords, }; /** @@ -38,6 +40,8 @@ export default async function triggerHandler(context) { const { log, data } = context; const { type, url } = data; + log.info(`AUDIT TRIGGERED ${type} ${url}`); + if (!hasText(type) || !hasText(url)) { return badRequest('required query params missing'); } diff --git a/src/controllers/trigger/common/trigger.js b/src/controllers/trigger/common/trigger.js index c090fb0d0..3802297cd 100644 --- a/src/controllers/trigger/common/trigger.js +++ b/src/controllers/trigger/common/trigger.js @@ -48,11 +48,12 @@ async function getSitesToAudit(dataAccess, url, deliveryType) { */ export async function triggerFromData(context, config, auditContext = {}) { try { - const { dataAccess, sqs } = context; + const { dataAccess, sqs, log } = context; const { AUDIT_JOBS_QUEUE_URL: queueUrl } = context.env; const { url, auditTypes, deliveryType } = config; const sitesToAudit = await getSitesToAudit(dataAccess, url, deliveryType); + log.info(`AUDIT is ${!sitesToAudit.length} for ${url}`); if (!sitesToAudit.length) { return notFound('Site not found'); } @@ -60,11 +61,15 @@ export async function triggerFromData(context, config, auditContext = {}) { const message = []; for (const auditType of auditTypes) { + log.info(`AUDIT is ${!auditType} for ${url}`); const sitesToAuditForType = sitesToAudit.filter((site) => { const auditConfig = site.getAuditConfig(); + log.info(`AUDIT is ${!auditConfig.getAuditTypeConfig(auditType)?.disabled()} for ${site.getId()}`); return !auditConfig.getAuditTypeConfig(auditType)?.disabled(); }); + log.info(`AUDIT is ${!sitesToAuditForType.length} for ${auditType}`); + if (!sitesToAuditForType.length) { message.push(`No site is enabled for ${auditType} audit type`); } else { @@ -76,6 +81,7 @@ export async function triggerFromData(context, config, auditContext = {}) { auditType, auditContext, sitesToAuditForType.map((site) => site.getId()), + config.log, ), ); } diff --git a/src/controllers/trigger/keywords.js b/src/controllers/trigger/keywords.js new file mode 100644 index 000000000..0c2d1889c --- /dev/null +++ b/src/controllers/trigger/keywords.js @@ -0,0 +1,55 @@ +/* + * Copyright 2024 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 { DELIVERY_TYPES } from '@adobe/spacecat-shared-data-access/src/models/site.js'; +import { internalServerError } from '@adobe/spacecat-shared-http-utils'; +import { triggerFromData } from './common/trigger.js'; +import { getSlackContext } from '../../utils/slack/base.js'; + +export const INITIAL_KEYWORDS_SLACK_MESSAGE = '*ORGANIC KEYWORDS REPORT* for the *last month* :thread:'; + +/** + * Triggers audit processes for websites based on the provided URL. + * + * @param {Object} context - The context object containing dataAccess, sqs, data, and env. + * @returns {Response} The response object with the audit initiation message or an error message. + */ +export default async function trigger(context) { + const { log } = context; + + const { type, url } = context.data; + const { + AUDIT_REPORT_SLACK_CHANNEL_ID: slackChannelId, + SLACK_BOT_TOKEN: token, + } = context.env; + + try { + const slackContext = await getSlackContext({ + slackChannelId, url, message: INITIAL_KEYWORDS_SLACK_MESSAGE, token, log, + }); + + const auditContext = { + slackContext, + }; + + const config = { + url, + auditTypes: [type], + deliveryType: DELIVERY_TYPES.AEM_EDGE, + }; + + return triggerFromData(context, config, auditContext); + } catch (e) { + log.error(`Failed to trigger ${type} audit for ${url}`, e); + return internalServerError(); + } +} diff --git a/src/support/utils.js b/src/support/utils.js index 7986cb0ce..9fcd3e235 100644 --- a/src/support/utils.js +++ b/src/support/utils.js @@ -64,8 +64,10 @@ export const sendAuditMessages = async ( type, auditContext, siteIDsToAudit, + log, ) => { for (const siteId of siteIDsToAudit) { + log?.info(`Triggered ${type} audit for ${siteIDsToAudit.length > 1 ? `all ${siteIDsToAudit.length} sites` : siteIDsToAudit[0]}`); // eslint-disable-next-line no-await-in-loop await sendAuditMessage(sqs, queueUrl, type, auditContext, siteId); } diff --git a/test/controllers/trigger/common/trigger.test.js b/test/controllers/trigger/common/trigger.test.js index ebe2260a6..cc93f13cb 100644 --- a/test/controllers/trigger/common/trigger.test.js +++ b/test/controllers/trigger/common/trigger.test.js @@ -64,6 +64,11 @@ describe('Trigger from data access', () => { sqs: sqsMock, data: { type: 'auditType', url: 'ALL' }, env: { AUDIT_JOBS_QUEUE_URL: 'http://sqs-queue-url.com' }, + log: { + info: sandbox.spy(), + warn: sandbox.spy(), + error: sandbox.spy(), + }, }; const config = { @@ -88,6 +93,11 @@ describe('Trigger from data access', () => { sqs: sqsMock, data: { type: 'auditType', url: 'ALL' }, env: { AUDIT_JOBS_QUEUE_URL: 'http://sqs-queue-url.com' }, + log: { + info: sandbox.spy(), + warn: sandbox.spy(), + error: sandbox.spy(), + }, }; const config = { @@ -112,6 +122,11 @@ describe('Trigger from data access', () => { sqs: sqsMock, data: { type: 'lhs', url: 'ALL' }, env: { AUDIT_JOBS_QUEUE_URL: 'http://sqs-queue-url.com' }, + log: { + info: sandbox.spy(), + warn: sandbox.spy(), + error: sandbox.spy(), + }, }; const config = { @@ -138,6 +153,11 @@ describe('Trigger from data access', () => { sqs: sqsMock, data: { type: 'auditType', url: 'http://site1.com' }, env: { AUDIT_JOBS_QUEUE_URL: 'http://sqs-queue-url.com' }, + log: { + info: sandbox.spy(), + warn: sandbox.spy(), + error: sandbox.spy(), + }, }; const config = { @@ -162,6 +182,11 @@ describe('Trigger from data access', () => { sqs: sqsMock, data: { type: 'auditType', url: 'https://example.com' }, env: { AUDIT_JOBS_QUEUE_URL: 'http://sqs-queue-url.com' }, + log: { + info: sandbox.spy(), + warn: sandbox.spy(), + error: sandbox.spy(), + }, }; const config = { @@ -185,6 +210,11 @@ describe('Trigger from data access', () => { sqs: sqsMock, data: { type: 'auditType', url: 'all' }, env: { AUDIT_JOBS_QUEUE_URL: 'http://sqs-queue-url.com' }, + log: { + info: sandbox.spy(), + warn: sandbox.spy(), + error: sandbox.spy(), + }, }; const config = { @@ -220,6 +250,11 @@ describe('Trigger from data access', () => { sqs: sqsMock, data: { type: 'auditType', url: 'all' }, env: { AUDIT_JOBS_QUEUE_URL: 'http://sqs-queue-url.com' }, + log: { + info: sandbox.spy(), + warn: sandbox.spy(), + error: sandbox.spy(), + }, }; const config = { @@ -251,6 +286,11 @@ describe('Trigger from data access', () => { sqs: sqsMock, data: { type: 'auditType', url: 'all' }, env: { AUDIT_JOBS_QUEUE_URL: 'http://sqs-queue-url.com' }, + log: { + info: sandbox.spy(), + warn: sandbox.spy(), + error: sandbox.spy(), + }, }; const config = { diff --git a/test/controllers/trigger/keywords.test.js b/test/controllers/trigger/keywords.test.js new file mode 100644 index 000000000..3a3a09035 --- /dev/null +++ b/test/controllers/trigger/keywords.test.js @@ -0,0 +1,107 @@ +/* + * Copyright 2024 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 { createSite } from '@adobe/spacecat-shared-data-access/src/models/site.js'; + +import { expect } from 'chai'; +import sinon from 'sinon'; + +import nock from 'nock'; +import { getQueryParams } from '../../../src/utils/slack/base.js'; +import trigger, { INITIAL_KEYWORDS_SLACK_MESSAGE } from '../../../src/controllers/trigger/keywords.js'; + +describe('Keywords trigger', () => { + let context; + let dataAccessMock; + let sqsMock; + let sandbox; + let sites; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + sites = [ + createSite({ + id: 'id1', + baseURL: 'https://foo.com', + auditConfig: { + auditTypeConfigs: { + 'organic-keywords': { + disabled: false, + }, + }, + }, + }), + createSite({ + id: 'id2', + baseURL: 'https://bar.com', + }), + ]; + + dataAccessMock = { + getSitesByDeliveryType: sandbox.stub(), + }; + + sqsMock = { + sendMessage: sandbox.stub().resolves(), + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('triggers a keyword audit', async () => { + context = { + log: console, + dataAccess: dataAccessMock, + sqs: sqsMock, + data: { type: 'organic-keywords', url: 'ALL' }, + env: { + AUDIT_JOBS_QUEUE_URL: 'http://sqs-queue-url.com', + SLACK_BOT_TOKEN: 'token', + AUDIT_REPORT_SLACK_CHANNEL_ID: 'channelId', + }, + }; + + dataAccessMock.getSitesByDeliveryType.resolves(sites); + + nock('https://slack.com') + .get('/api/chat.postMessage') + .query(getQueryParams('channelId', INITIAL_KEYWORDS_SLACK_MESSAGE)) + .reply(200, { + ok: true, + channel: 'channelId', + ts: 'threadId', + }); + + const response = await trigger(context); + const result = await response.json(); + + expect(dataAccessMock.getSitesByDeliveryType.calledOnce).to.be.true; + expect(sqsMock.sendMessage.callCount).to.equal(1); + expect(result.message[0]).to.equal('Triggered organic-keywords audit for id1'); + }); + + it('throws an error if slack post message fails', async () => { + nock('https://slack.com') + .get('/api/chat.postMessage') + .query(getQueryParams('channelId', INITIAL_KEYWORDS_SLACK_MESSAGE)) + .replyWithError('Slack post message API request failed'); + + const response = await trigger(context); + + expect(response.status).to.equal(500); + }); +}); diff --git a/test/controllers/trigger/lhs.test.js b/test/controllers/trigger/lhs.test.js index b49c900ed..3d3503581 100644 --- a/test/controllers/trigger/lhs.test.js +++ b/test/controllers/trigger/lhs.test.js @@ -63,6 +63,11 @@ describe('LHS Trigger', () => { sqs: sqsMock, data: { type: 'lhs-mobile', url: 'ALL' }, env: { AUDIT_JOBS_QUEUE_URL: 'http://sqs-queue-url.com' }, + log: { + info: sandbox.spy(), + warn: sandbox.spy(), + error: sandbox.spy(), + }, }; dataAccessMock.getSites.resolves(sites); @@ -81,6 +86,11 @@ describe('LHS Trigger', () => { sqs: sqsMock, data: { type: 'lhs', url: 'ALL' }, env: { AUDIT_JOBS_QUEUE_URL: 'http://sqs-queue-url.com' }, + log: { + info: sandbox.spy(), + warn: sandbox.spy(), + error: sandbox.spy(), + }, }; dataAccessMock.getSites.resolves(sites);