Skip to content

Commit 9f2f75a

Browse files
feat: add batch-disable-import-audit-job handler and full test coverage
- Add batch-disable-import-audit-job handler that sits between the Map state and notifier in the SFN workflow; disables import types and audit handlers for each site sequentially to avoid concurrent config write races; passes siteResults through unchanged for the downstream notifier - Expand batch-opportunity-status-job handler tests to cover all branches (malformed opp, unexpected error, data source failures, null suggestions) - Add notifier tests covering all formatting paths, Slack error handling, and the formatSlackSummary error catch branch - Add trigger-opportunity-status-job handler tests covering resolveExpectedOpportunityTypes, wasUpdatedByAudit, and all handler paths Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e3889d1 commit 9f2f75a

6 files changed

Lines changed: 1290 additions & 17 deletions

File tree

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { runSlackNotify as slackNotify } from './tasks/slack-notify/handler.js';
3131
import { runTriggerOpportunityStatusJob as triggerOpportunityStatusJob } from './tasks/trigger-opportunity-status-job/handler.js';
3232
import { runBatchOpportunityStatusJob as batchOpportunityStatusJob } from './tasks/batch-opportunity-status-job/handler.js';
3333
import { runBatchOpportunityStatusNotifier as batchOpportunityStatusNotifier } from './tasks/batch-opportunity-status-job/notifier.js';
34+
import { runBatchDisableImportAuditJob as batchDisableImportAuditJob } from './tasks/batch-opportunity-status-job/disable-import-audit.js';
3435

3536
const HANDLERS = {
3637
'opportunity-status-processor': opportunityStatusProcessor,
@@ -41,6 +42,7 @@ const HANDLERS = {
4142
'cwv-demo-suggestions-processor': cwvDemoSuggestionsProcessor,
4243
'trigger-opportunity-status-job': triggerOpportunityStatusJob,
4344
'batch-opportunity-status-job': batchOpportunityStatusJob,
45+
'batch-disable-import-audit-job': batchDisableImportAuditJob,
4446
'batch-opportunity-status-notifier': batchOpportunityStatusNotifier,
4547
dummy: (message) => ok(message), // for tests
4648
};
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Copyright 2025 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import { ok } from '@adobe/spacecat-shared-http-utils';
14+
import { Config } from '@adobe/spacecat-shared-data-access';
15+
16+
const TASK_TYPE = 'batch-disable-import-audit-job';
17+
18+
/**
19+
* Disables the specified import types and audit handlers for a single site.
20+
*
21+
* @param {string} siteId
22+
* @param {string} baseUrl
23+
* @param {string[]} importTypes
24+
* @param {string[]} auditTypes
25+
* @param {object} dataAccess
26+
* @param {object} log
27+
* @returns {Promise<{siteId: string, baseUrl: string, disabled: boolean, reason?: string}>}
28+
*/
29+
async function disableSite(siteId, baseUrl, importTypes, auditTypes, dataAccess, log) {
30+
const { Site, Configuration } = dataAccess;
31+
32+
let site;
33+
try {
34+
site = await Site.findById(siteId);
35+
} catch (err) {
36+
log.error(`[${TASK_TYPE}] Failed to fetch site ${siteId}: ${err.message}`);
37+
return {
38+
siteId, baseUrl, disabled: false, reason: `DB error: ${err.message}`,
39+
};
40+
}
41+
42+
if (!site) {
43+
log.warn(`[${TASK_TYPE}] Site not found: ${siteId}`);
44+
return {
45+
siteId, baseUrl, disabled: false, reason: 'site not found',
46+
};
47+
}
48+
49+
try {
50+
const siteConfig = site.getConfig();
51+
for (const importType of importTypes) {
52+
siteConfig.disableImport(importType);
53+
}
54+
site.setConfig(Config.toDynamoItem(siteConfig));
55+
56+
const configuration = await Configuration.findLatest();
57+
for (const auditType of auditTypes) {
58+
configuration.disableHandlerForSite(auditType, site);
59+
}
60+
61+
await site.save();
62+
await configuration.save();
63+
64+
log.info(`[${TASK_TYPE}] Disabled imports=[${importTypes}] audits=[${auditTypes}] for site ${siteId} (${baseUrl})`);
65+
return { siteId, baseUrl, disabled: true };
66+
} catch (err) {
67+
log.error(`[${TASK_TYPE}] Failed to disable site ${siteId}: ${err.message}`);
68+
return {
69+
siteId, baseUrl, disabled: false, reason: err.message,
70+
};
71+
}
72+
}
73+
74+
/**
75+
* Runs the batch disable-import-and-audit job.
76+
*
77+
* Invoked by the Step Functions workflow after the per-site Map state completes.
78+
* Iterates over all site results, disables the specified import types and audit
79+
* handlers for each site, then passes the original siteResults through unchanged
80+
* so the downstream notifier can format its Slack summary.
81+
*
82+
* Skips all disabling when taskContext.scheduledRun is true.
83+
*
84+
* Message shape (sent by SFN after the Map state):
85+
* {
86+
* type: 'batch-disable-import-audit-job',
87+
* siteResults: Array<{ result: { siteId, baseUrl, found[], notFound[], dataSources } }>,
88+
* taskContext: {
89+
* importTypes: string[], // import types to disable for every site
90+
* auditTypes: string[], // audit handlers to disable for every site
91+
* scheduledRun: boolean, // when true, skip all disabling
92+
* slackContext?: { channelId: string, threadTs: string }
93+
* }
94+
* }
95+
*
96+
* @param {object} message - Lambda payload from the SFN
97+
* @param {object} context - Universal serverless context
98+
*/
99+
export async function runBatchDisableImportAuditJob(message, context) {
100+
const { log, dataAccess } = context;
101+
const { siteResults = [], taskContext = {} } = message;
102+
const {
103+
importTypes = [],
104+
auditTypes = [],
105+
scheduledRun = false,
106+
} = taskContext;
107+
108+
const sites = Array.isArray(siteResults)
109+
? siteResults.map((item) => item?.result).filter(Boolean)
110+
: [];
111+
112+
log.info(`[${TASK_TYPE}] Starting — sites=${sites.length} scheduledRun=${scheduledRun}`, {
113+
importTypes,
114+
auditTypes,
115+
});
116+
117+
if (scheduledRun) {
118+
log.info(`[${TASK_TYPE}] Scheduled run — skipping disable of imports and audits`);
119+
return ok({
120+
message: 'Scheduled run — no imports or audits disabled',
121+
siteResults,
122+
disableResults: [],
123+
});
124+
}
125+
126+
if (importTypes.length === 0 && auditTypes.length === 0) {
127+
log.info(`[${TASK_TYPE}] No importTypes or auditTypes specified — nothing to disable`);
128+
return ok({ message: 'Nothing to disable', siteResults, disableResults: [] });
129+
}
130+
131+
// Process sites sequentially to avoid hammering the DB with parallel config saves
132+
const disableResults = [];
133+
for (const site of sites) {
134+
// eslint-disable-next-line no-await-in-loop
135+
const result = await disableSite(
136+
site.siteId,
137+
site.baseUrl,
138+
importTypes,
139+
auditTypes,
140+
dataAccess,
141+
log,
142+
);
143+
disableResults.push(result);
144+
}
145+
146+
const succeeded = disableResults.filter((r) => r.disabled).length;
147+
const failed = disableResults.filter((r) => !r.disabled).length;
148+
149+
log.info(`[${TASK_TYPE}] Done — disabled=${succeeded} failed=${failed}`);
150+
151+
return ok({
152+
message: `Batch disable completed: ${succeeded} succeeded, ${failed} failed`,
153+
siteResults, // pass-through for the downstream notifier
154+
disableResults,
155+
});
156+
}
157+
158+
export default runBatchDisableImportAuditJob;

0 commit comments

Comments
 (0)