@@ -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
0 commit comments