99 buildDemographicsPayload ,
1010 demographicsContentMeta ,
1111 demographicsStageCoverageMeta ,
12+ fetchWppStage ,
1213 parseIlostatWorkforceCsv ,
1314 parseWorldBankEducation ,
1415 parseWppCapability ,
@@ -24,6 +25,26 @@ import { resolveSourceOrigin } from '../scripts/source-origin.mjs';
2425const fixture = ( name ) => readFileSync ( new URL ( `./fixtures/demographics-capability/${ name } ` , import . meta. url ) , 'utf8' ) ;
2526const repoFile = ( name ) => readFileSync ( new URL ( `../${ name } ` , import . meta. url ) , 'utf8' ) ;
2627
28+ function wppResponse ( url , currentYear = 2026 ) {
29+ const requestUrl = String ( url ) ;
30+ const locations = requestUrl . match ( / \/ l o c a t i o n s \/ ( [ ^ / ] + ) \/ / ) ?. [ 1 ] . split ( ',' ) . map ( Number ) || [ ] ;
31+ const shared = { variantId : 4 , sexId : 3 } ;
32+ const rows = requestUrl . includes ( '/indicators/70/' )
33+ ? locations . flatMap ( ( locationId ) => [
34+ { ...shared , locationId, indicatorId : 70 , ageId : 40 , timeLabel : currentYear , value : 1_000_000 } ,
35+ { ...shared , locationId, indicatorId : 70 , ageId : 40 , timeLabel : currentYear + 10 , value : 900_000 } ,
36+ ] )
37+ : locations . flatMap ( ( locationId ) => [
38+ { ...shared , locationId, indicatorId : 67 , ageId : 188 , timeLabel : currentYear , value : 40 } ,
39+ { ...shared , locationId, indicatorId : 84 , ageId : 1005 , timeLabel : currentYear , value : 20 } ,
40+ { ...shared , locationId, indicatorId : 86 , ageId : 1015 , timeLabel : currentYear , value : 50 } ,
41+ ] ) ;
42+ return new Response ( JSON . stringify ( rows ) , {
43+ status : 200 ,
44+ headers : { 'Content-Type' : 'application/json' } ,
45+ } ) ;
46+ }
47+
2748describe ( 'demographics capability source parsers (#6437)' , ( ) => {
2849 it ( 'parses real WPP JSON without turning null into zero' , ( ) => {
2950 const raw = JSON . parse ( fixture ( 'wpp.json' ) ) ;
@@ -76,6 +97,49 @@ describe('demographics capability source parsers (#6437)', () => {
7697 }
7798 assert . equal ( validateDemographicsStageCoverage ( countries , 'ilostat' ) . trainedIndustrialWorkforcePeople , 150 ) ;
7899 } ) ;
100+
101+ it ( 'recovers after two consecutive HTTP 502 responses from one WPP page' , async ( ) => {
102+ const targetPage = '/indicators/67,84,86/locations/100,104,' ;
103+ let targetPageRequests = 0 ;
104+ const result = await fetchWppStage ( {
105+ currentYear : 2026 ,
106+ fetchImpl : async ( url ) => {
107+ if ( String ( url ) . includes ( targetPage ) ) {
108+ targetPageRequests += 1 ;
109+ if ( targetPageRequests <= 2 ) return new Response ( 'Bad Gateway' , { status : 502 } ) ;
110+ }
111+ return wppResponse ( url ) ;
112+ } ,
113+ } ) ;
114+
115+ assert . equal ( targetPageRequests , 3 ) ;
116+ const recordCount = Object . keys ( result . countries ) . length ;
117+ assert . ok ( recordCount >= 229 ) ;
118+ assert . equal ( validateDemographicsStageCoverage ( result . countries , 'wpp' ) . medianAgeYears , recordCount ) ;
119+ } ) ;
120+
121+ it ( 'identifies the exact WPP page when its HTTP failure is exhausted' , async ( ) => {
122+ const failedPage = '/indicators/70/locations/288,292,' ;
123+ let failedPageRequests = 0 ;
124+ await assert . rejects (
125+ fetchWppStage ( {
126+ currentYear : 2026 ,
127+ fetchImpl : async ( url ) => {
128+ if ( String ( url ) . includes ( failedPage ) ) {
129+ failedPageRequests += 1 ;
130+ return new Response ( 'Bad Gateway' , { status : 502 } ) ;
131+ }
132+ return wppResponse ( url ) ;
133+ } ,
134+ } ) ,
135+ ( error ) => {
136+ assert . match ( error . message , / H T T P 5 0 2 / ) ;
137+ assert . match ( error . message , new RegExp ( failedPage ) ) ;
138+ return true ;
139+ } ,
140+ ) ;
141+ assert . equal ( failedPageRequests , 4 ) ;
142+ } ) ;
79143} ) ;
80144
81145describe ( 'demographics partial-stage publication' , ( ) => {
0 commit comments