Skip to content

Commit 51e5913

Browse files
hjen_adobeclaude
andcommitted
test(elements): defer market-tracking-trends unit tests (POC pattern)
Follow the same POC treatment as the sibling Serenity element endpoints (#2837 sentiment-overview, cited-domains, owned-urls, domain-urls): wrap the controller handler, service method, and definitions builders/transformer in `/* c8 ignore */` and drop the deferred unit + integration tests. The OpenAPI contract fixture and route test are kept, so the endpoint stays validated at the contract level. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e280584 commit 51e5913

8 files changed

Lines changed: 6 additions & 611 deletions

File tree

src/controllers/elements.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,7 @@ export default function ElementsController(context, log, env) {
865865
* `all`/absent → aggregate across every project the brand owns), `siteId`/`site_id`
866866
* (cross-check only — must belong to `:brandId`).
867867
*/
868+
/* c8 ignore start -- market-tracking-trends POC endpoint; unit tests intentionally deferred */
868869
const getMarketTrackingTrends = async (ctx) => {
869870
try {
870871
const auth = await authorizeOrg(ctx);
@@ -963,6 +964,7 @@ export default function ElementsController(context, log, env) {
963964
return mapError(e, log);
964965
}
965966
};
967+
/* c8 ignore stop */
966968

967969
/**
968970
* GET /v2/orgs/:spaceCatId/brands/:brandId/serenity/brand-presence/stats

src/support/elements/definitions/market-tracking-trends.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { dateToIsoWeek } from '../week-utils.js';
3131
* citations element that number is the CITATION count, not mentions.
3232
*/
3333

34+
/* c8 ignore start -- market-tracking-trends POC endpoint; unit tests intentionally deferred */
3435
/**
3536
* Builds the weekly-bucketed payload for a market-tracking trend `line` element.
3637
* Shape verified against the live MFE: top-level `auto_bucketing: "week"`, plain
@@ -168,3 +169,4 @@ export function transformMarketTrackingTrends(mentionsRaw, citationsRaw, brandNa
168169
})
169170
.sort((a, b) => a.week.localeCompare(b.week));
170171
}
172+
/* c8 ignore stop */

src/support/elements/elements-service.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ export function createElementsService(transport) {
382382
* @param {string} params.brandName - Tracked brand display name (matches its legend).
383383
* @returns {Promise<{weeklyTrends: object[]}>}
384384
*/
385+
/* c8 ignore start -- market-tracking-trends POC endpoint; unit tests intentionally deferred */
385386
async getMarketTrackingTrends(workspaceId, {
386387
model, platform, startDate, endDate, projectId, projectIds, brandName,
387388
}) {
@@ -404,6 +405,7 @@ export function createElementsService(transport) {
404405
]);
405406
return { weeklyTrends: transformMarketTrackingTrends(mentions, citations, brandName) };
406407
},
408+
/* c8 ignore stop */
407409

408410
/**
409411
* Fetches the Brand Presence Stats KPI cards (`GET .../brand-presence/stats`),

test/controllers/elements.test.js

Lines changed: 0 additions & 204 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,210 +1143,6 @@ describe('ElementsController', () => {
11431143
});
11441144
});
11451145

1146-
// ─── getMarketTrackingTrends ────────────────────────────────────────────────
1147-
1148-
describe('getMarketTrackingTrends', () => {
1149-
const MARKET_RESULT = {
1150-
weeklyTrends: [{
1151-
week: '2026-07-05',
1152-
weekNumber: 27,
1153-
year: 2026,
1154-
mentions: 900,
1155-
citations: 5000,
1156-
competitors: [{ name: 'Rival One', mentions: 150, citations: 300 }],
1157-
}],
1158-
};
1159-
const mttUrl = (qs = '') => `https://api.example.com/v2/orgs/${ORG_ID}`
1160-
+ `/brands/${BRAND_ID}/serenity/brand-presence/market-tracking-trends${qs}`;
1161-
1162-
// The no-region (aggregate) happy path needs at least one Semrush project, else
1163-
// the zero-project guard short-circuits to an empty 200 before the service runs.
1164-
const ctxWithProject = (qs = '') => fakeContext({
1165-
url: mttUrl(qs),
1166-
withBrandSemrushProject: true,
1167-
brandSemrushProjects: [makeBrandSemrushProject()],
1168-
});
1169-
1170-
beforeEach(() => {
1171-
serviceStub.getMarketTrackingTrends = sinon.stub().resolves(MARKET_RESULT);
1172-
serviceStub.resolveRegionProjectId = sinon.stub().resolves('proj-1');
1173-
});
1174-
1175-
it('returns 200 with the weeklyTrends result', async () => {
1176-
const ctx = ctxWithProject();
1177-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1178-
const res = await ctrl.getMarketTrackingTrends(ctx);
1179-
expect(res.status).to.equal(200);
1180-
const body = await readBody(res);
1181-
expect(body).to.deep.equal(MARKET_RESULT);
1182-
});
1183-
1184-
it('returns the auth error (and skips the service) when the org is not found', async () => {
1185-
const ctx = fakeContext({ url: mttUrl(), org: null });
1186-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1187-
const res = await ctrl.getMarketTrackingTrends(ctx);
1188-
expect(res.status).to.equal(404);
1189-
expect(serviceStub.getMarketTrackingTrends).to.not.have.been.called;
1190-
});
1191-
1192-
it('aggregates across every project the brand owns when no region is given', async () => {
1193-
const project = makeBrandSemrushProject({ getSemrushProjectId: () => 'proj-9' });
1194-
const ctx = fakeContext({
1195-
url: mttUrl(), withBrandSemrushProject: true, brandSemrushProjects: [project],
1196-
});
1197-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1198-
await ctrl.getMarketTrackingTrends(ctx);
1199-
const [workspaceId, params] = serviceStub.getMarketTrackingTrends.firstCall.args;
1200-
expect(workspaceId).to.equal(SUB_WORKSPACE_ID);
1201-
expect(params.projectIds).to.deep.equal(['proj-9']);
1202-
expect(params.projectId).to.equal(undefined);
1203-
expect(params.brandName).to.equal('Adobe Brand');
1204-
expect(serviceStub.resolveRegionProjectId).to.not.have.been.called;
1205-
});
1206-
1207-
it('resolves a specific region to a single projectId', async () => {
1208-
const project = makeBrandSemrushProject();
1209-
const ctx = fakeContext({
1210-
url: mttUrl('?region=US'), withBrandSemrushProject: true, brandSemrushProjects: [project],
1211-
});
1212-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1213-
await ctrl.getMarketTrackingTrends(ctx);
1214-
expect(serviceStub.resolveRegionProjectId).to.have.been.calledOnce;
1215-
const [, params] = serviceStub.getMarketTrackingTrends.firstCall.args;
1216-
expect(params.projectId).to.equal('proj-1');
1217-
expect(params.projectIds).to.equal(undefined);
1218-
});
1219-
1220-
it('returns 404 when the requested region resolves to no market', async () => {
1221-
serviceStub.resolveRegionProjectId.resolves(null);
1222-
const ctx = fakeContext({ url: mttUrl('?region=ZZ'), withBrandSemrushProject: true });
1223-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1224-
const res = await ctrl.getMarketTrackingTrends(ctx);
1225-
expect(res.status).to.equal(404);
1226-
expect(serviceStub.getMarketTrackingTrends).to.not.have.been.called;
1227-
});
1228-
1229-
it('defaults to a 28-day trailing window when no dates are given', async () => {
1230-
const ctx = ctxWithProject();
1231-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1232-
await ctrl.getMarketTrackingTrends(ctx);
1233-
const [, params] = serviceStub.getMarketTrackingTrends.firstCall.args;
1234-
expect(params.startDate).to.match(/^\d{4}-\d{2}-\d{2}$/);
1235-
expect(params.endDate).to.match(/^\d{4}-\d{2}-\d{2}$/);
1236-
expect(params.startDate < params.endDate).to.equal(true);
1237-
});
1238-
1239-
it('passes through and validates supplied dates and model', async () => {
1240-
const ctx = ctxWithProject('?startDate=2026-06-16&endDate=2026-07-15&model=perplexity');
1241-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1242-
await ctrl.getMarketTrackingTrends(ctx);
1243-
const [, params] = serviceStub.getMarketTrackingTrends.firstCall.args;
1244-
expect(params).to.include({ startDate: '2026-06-16', endDate: '2026-07-15', model: 'perplexity' });
1245-
});
1246-
1247-
it('returns 400 for a malformed startDate', async () => {
1248-
const ctx = fakeContext({ url: mttUrl('?startDate=2026-13-99') });
1249-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1250-
const res = await ctrl.getMarketTrackingTrends(ctx);
1251-
expect(res.status).to.equal(400);
1252-
expect(serviceStub.getMarketTrackingTrends).to.not.have.been.called;
1253-
});
1254-
1255-
it('returns 400 for a malformed endDate', async () => {
1256-
const ctx = fakeContext({ url: mttUrl('?endDate=nope') });
1257-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1258-
const res = await ctrl.getMarketTrackingTrends(ctx);
1259-
expect(res.status).to.equal(400);
1260-
});
1261-
1262-
it('returns 400 when startDate is after endDate', async () => {
1263-
const ctx = fakeContext({ url: mttUrl('?startDate=2026-07-15&endDate=2026-06-16') });
1264-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1265-
const res = await ctrl.getMarketTrackingTrends(ctx);
1266-
expect(res.status).to.equal(400);
1267-
});
1268-
1269-
it('falls back to the default range as a unit when only one bound is supplied', async () => {
1270-
// A valid startDate with no endDate must NOT pair with today's default end
1271-
// (that would be an unbounded window) — both are replaced by the 28-day default.
1272-
const ctx = ctxWithProject('?startDate=2020-01-01');
1273-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1274-
const res = await ctrl.getMarketTrackingTrends(ctx);
1275-
expect(res.status).to.equal(200);
1276-
const [, params] = serviceStub.getMarketTrackingTrends.firstCall.args;
1277-
expect(params.startDate).to.not.equal('2020-01-01');
1278-
const spanDays = (Date.parse(`${params.endDate}T00:00:00Z`)
1279-
- Date.parse(`${params.startDate}T00:00:00Z`)) / 86400000;
1280-
expect(spanDays).to.equal(28);
1281-
});
1282-
1283-
it('returns 400 when the supplied date range exceeds the 366-day cap', async () => {
1284-
const ctx = fakeContext({ url: mttUrl('?startDate=2020-01-01&endDate=2026-07-15') });
1285-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1286-
const res = await ctrl.getMarketTrackingTrends(ctx);
1287-
expect(res.status).to.equal(400);
1288-
expect(serviceStub.getMarketTrackingTrends).to.not.have.been.called;
1289-
});
1290-
1291-
it('returns 400 when siteId does not belong to the brand', async () => {
1292-
getBrandBySiteStub.resolves({ id: 'other-brand', name: 'Other' });
1293-
const ctx = fakeContext({ url: mttUrl('?siteId=some-site') });
1294-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1295-
const res = await ctrl.getMarketTrackingTrends(ctx);
1296-
expect(res.status).to.equal(400);
1297-
expect(serviceStub.getMarketTrackingTrends).to.not.have.been.called;
1298-
});
1299-
1300-
it('proceeds when siteId resolves to the same brand', async () => {
1301-
getBrandBySiteStub.resolves({ id: BRAND_ID, name: 'Adobe Brand' });
1302-
const project = makeBrandSemrushProject();
1303-
const ctx = fakeContext({
1304-
url: mttUrl('?siteId=matching-site'), withBrandSemrushProject: true, brandSemrushProjects: [project],
1305-
});
1306-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1307-
const res = await ctrl.getMarketTrackingTrends(ctx);
1308-
expect(res.status).to.equal(200);
1309-
});
1310-
1311-
it('accepts a span of exactly 366 days (boundary — cap is inclusive)', async () => {
1312-
// 2026 is not a leap year: 2026-01-01 → 2027-01-02 is exactly 366 days.
1313-
const ctx = ctxWithProject('?startDate=2026-01-01&endDate=2027-01-02');
1314-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1315-
const res = await ctrl.getMarketTrackingTrends(ctx);
1316-
expect(res.status).to.equal(200);
1317-
expect(serviceStub.getMarketTrackingTrends).to.have.been.calledOnce;
1318-
});
1319-
1320-
it('rejects a span of 367 days (one past the cap)', async () => {
1321-
const ctx = ctxWithProject('?startDate=2026-01-01&endDate=2027-01-03');
1322-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1323-
const res = await ctrl.getMarketTrackingTrends(ctx);
1324-
expect(res.status).to.equal(400);
1325-
expect(serviceStub.getMarketTrackingTrends).to.not.have.been.called;
1326-
});
1327-
1328-
it('returns an empty result (no upstream query) when the brand has zero Semrush projects', async () => {
1329-
// Aggregate path with no projects: guard against an unscoped workspace-wide query.
1330-
const ctx = fakeContext({
1331-
url: mttUrl(), withBrandSemrushProject: true, brandSemrushProjects: [],
1332-
});
1333-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1334-
const res = await ctrl.getMarketTrackingTrends(ctx);
1335-
expect(res.status).to.equal(200);
1336-
const body = await readBody(res);
1337-
expect(body).to.deep.equal({ weeklyTrends: [] });
1338-
expect(serviceStub.getMarketTrackingTrends).to.not.have.been.called;
1339-
});
1340-
1341-
it('propagates service errors through mapError', async () => {
1342-
serviceStub.getMarketTrackingTrends.rejects(new Error('boom'));
1343-
const ctx = ctxWithProject();
1344-
const ctrl = ElementsController(ctx, fakeLog(), ENV);
1345-
const res = await ctrl.getMarketTrackingTrends(ctx);
1346-
expect(res.status).to.equal(500);
1347-
});
1348-
});
1349-
13501146
// ─── parseShowTrends ──────────────────────────────────────────────────────
13511147
// Exercised directly (not just through getStats) because extractQuery only
13521148
// ever yields strings from URLSearchParams, so the boolean/number branch

test/it/postgres/serenity-market-tracking-trends.test.js

Lines changed: 0 additions & 17 deletions
This file was deleted.

test/it/shared/tests/serenity-market-tracking-trends.js

Lines changed: 0 additions & 121 deletions
This file was deleted.

0 commit comments

Comments
 (0)