Skip to content

Commit 5c1255b

Browse files
committed
modified tests to output warnings for variances due to dev enhancements, where they are not necessarily bugs
1 parent 8a88734 commit 5c1255b

File tree

2 files changed

+189
-74
lines changed

2 files changed

+189
-74
lines changed

api-tests/v2Tests/defs_projects.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ it("should return metadata", function (done) {
1515
});
1616

1717
it("should return a sample", function (done) {
18-
request(settings.host)
18+
this.timeout(10000);
19+
request(settings.host)
1920
.get("/defs/projects?sample")
2021
.expect(validators.aSuccessfulRequest)
2122
.expect(validators.json)
@@ -27,7 +28,8 @@ it("should return a sample", function (done) {
2728
});
2829

2930
it("should return all projects", function (done) {
30-
request(settings.host)
31+
this.timeout(10000);
32+
request(settings.host)
3133
.get("/defs/projects?all")
3234
.expect(validators.aSuccessfulRequest)
3335
.expect(validators.json)
@@ -39,7 +41,8 @@ it("should return all projects", function (done) {
3941
});
4042

4143
it("should output CSV", function (done) {
42-
request(settings.host)
44+
this.timeout(10000);
45+
request(settings.host)
4346
.get("/defs/projects?all&format=csv")
4447
.expect(validators.aSuccessfulRequest)
4548
.expect(validators.csv)

api-tests/validators.ts

Lines changed: 183 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,41 @@
22
const axios = require("axios");
33
const settings = require("./settings");
44

5+
6+
function sortKeysDeep(x) {
7+
if (Array.isArray(x)) return x.map(sortKeysDeep);
8+
if (x && typeof x === "object") {
9+
return Object.keys(x)
10+
.sort()
11+
.reduce((acc, k) => {
12+
acc[k] = sortKeysDeep(x[k]);
13+
return acc;
14+
}, {});
15+
}
16+
return x;
17+
}
18+
19+
20+
function isMetadataPayload(body) {
21+
return (
22+
body &&
23+
body.success &&
24+
body.success.options &&
25+
body.success.options.parameters &&
26+
typeof body.success.options.parameters === "object"
27+
);
28+
}
29+
30+
function diffKeys(aObj = {}, bObj = {}) {
31+
const aKeys = new Set(Object.keys(aObj || {}));
32+
const bKeys = new Set(Object.keys(bObj || {}));
33+
const onlyInA = [...aKeys].filter((k) => !bKeys.has(k)).sort();
34+
const onlyInB = [...bKeys].filter((k) => !aKeys.has(k)).sort();
35+
return { onlyInA, onlyInB };
36+
}
37+
38+
39+
540
function extractArrayData(payload) {
641
try {
742
if (!payload || !payload.success) return null;
@@ -216,10 +251,10 @@ module.exports = {
216251

217252
res.body.success.data.features.forEach(function (d, index) {
218253
if (
219-
!d.geometry ||
220-
!d.geometry.coordinates ||
221-
!d.geometry.coordinates.length ||
222-
!d.properties
254+
!d.geometry ||
255+
!d.geometry.coordinates ||
256+
!d.geometry.coordinates.length ||
257+
!d.properties
223258
) {
224259
console.error("Malformed feature at index:", index, d); // Log issue for inspection
225260
throw new Error(`GeoJSON was malformed at feature index ${index}`);
@@ -269,91 +304,168 @@ module.exports = {
269304
correctDataTypes: function (res: any) {
270305
const data = res.body.success.data;
271306
data.forEach(
272-
(item: { t_age: any; b_age: any; t_prop?: any; t_int_age?: any }) => {
273-
if (typeof item.t_age !== "number") {
274-
throw new Error(
275-
`t_age is not a numeric type: ${item.t_age} (type: ${typeof item.t_age})`,
276-
);
277-
}
278-
if (typeof item.b_age !== "number") {
279-
throw new Error(
280-
`b_age is not a numeric type: ${item.b_age} (type: ${typeof item.b_age})`,
281-
);
282-
}
283-
if (item.t_prop !== undefined && typeof item.t_prop !== "number") {
284-
throw new Error(
285-
`t_prop is not a numeric type: ${item.t_prop} (type: ${typeof item.t_prop})`,
286-
);
287-
}
288-
if (
289-
item.t_int_age !== undefined &&
290-
typeof item.t_int_age !== "number"
291-
) {
292-
throw new Error(
293-
`t_int_age is not a numeric type: ${item.t_int_age} (type: ${typeof item.t_int_age})`,
294-
);
295-
}
296-
},
307+
(item: { t_age: any; b_age: any; t_prop?: any; t_int_age?: any }) => {
308+
if (typeof item.t_age !== "number") {
309+
throw new Error(
310+
`t_age is not a numeric type: ${item.t_age} (type: ${typeof item.t_age})`,
311+
);
312+
}
313+
if (typeof item.b_age !== "number") {
314+
throw new Error(
315+
`b_age is not a numeric type: ${item.b_age} (type: ${typeof item.b_age})`,
316+
);
317+
}
318+
if (item.t_prop !== undefined && typeof item.t_prop !== "number") {
319+
throw new Error(
320+
`t_prop is not a numeric type: ${item.t_prop} (type: ${typeof item.t_prop})`,
321+
);
322+
}
323+
if (
324+
item.t_int_age !== undefined &&
325+
typeof item.t_int_age !== "number"
326+
) {
327+
throw new Error(
328+
`t_int_age is not a numeric type: ${item.t_int_age} (type: ${typeof item.t_int_age})`,
329+
);
330+
}
331+
},
297332
);
298333
},
334+
async compareWithProduction(requestPath = "", localResponse) {
335+
const { host_prod } = settings;
336+
if (!host_prod) return;
299337

300-
async compareWithProduction(requestPath = "", localResponse) {
301-
const { host_prod } = settings;
338+
const prodUrl = host_prod + requestPath;
339+
const { data: prodData } = await axios.get(prodUrl);
302340

303-
if (!host_prod) {
304-
// Skip comparison if no production host is set
305-
return;
306-
}
341+
// Helpers (kept inside the function to keep the diff localized)
342+
function keysOfItem(item) {
343+
const base =
344+
item?.properties && typeof item.properties === "object"
345+
? item.properties
346+
: item;
347+
return base && typeof base === "object" ? Object.keys(base) : [];
348+
}
307349

308-
const prodUrl = host_prod + requestPath;
309-
const { data: prodData } = await axios.get(prodUrl);
350+
function diffItemKeys(localArr, prodArr) {
351+
const localKeys = new Set(keysOfItem(localArr[0]));
352+
const prodKeys = new Set(keysOfItem(prodArr[0]));
353+
const onlyInLocal = [...localKeys].filter((k) => !prodKeys.has(k)).sort();
354+
const onlyInProd = [...prodKeys].filter((k) => !localKeys.has(k)).sort();
355+
return { onlyInLocal, onlyInProd };
356+
}
357+
358+
function sameIdSet(aSet, bSet) {
359+
if (aSet.size !== bSet.size) return false;
360+
for (const v of aSet) if (!bSet.has(v)) return false;
361+
return true;
362+
}
363+
364+
// 1) Canonical deep-equality (order-insensitive)
365+
if (
366+
JSON.stringify(sortKeysDeep(localResponse.body)) ===
367+
JSON.stringify(sortKeysDeep(prodData))
368+
) {
369+
return;
370+
}
371+
372+
// 2) Lenient path: metadata variance (warn, don't fail)
373+
if (isMetadataPayload(localResponse.body) && isMetadataPayload(prodData)) {
374+
const localParams = localResponse.body.success.options.parameters;
375+
const prodParams = prodData.success.options.parameters;
310376

311-
// Exact JSON match still passes quickly.
312-
if (JSON.stringify(localResponse.body) === JSON.stringify(prodData)) {
377+
const { onlyInA: onlyInLocal, onlyInB: onlyInProd } = diffKeys(
378+
localParams,
379+
prodParams,
380+
);
381+
382+
if (onlyInLocal.length || onlyInProd.length) {
383+
console.warn(
384+
[
385+
`⚠️ metadata parameters mismatch for endpoint: ${requestPath}`,
386+
` - Only in Dev (current host): ${
387+
onlyInLocal.length ? onlyInLocal.join(", ") : "(none)"
388+
}`,
389+
` - Only in Prod (host_prod): ${
390+
onlyInProd.length ? onlyInProd.join(", ") : "(none)"
391+
}`,
392+
` → Status: OK (metadata drift tolerated)`,
393+
].join("\n"),
394+
);
313395
return;
314396
}
397+
// If no param-key diffs but still not equal, fall through (real mismatch elsewhere)
398+
}
315399

316-
// Lenient path for array-like payloads
317-
const localArr = extractArrayData(localResponse.body);
318-
const prodArr = extractArrayData(prodData);
400+
// 3) Existing lenient path for arrays (idKey count checks, sample logic, etc.)
401+
const localArr = extractArrayData(localResponse.body);
402+
const prodArr = extractArrayData(prodData);
319403

320-
if (
321-
Array.isArray(localArr) &&
322-
Array.isArray(prodArr) &&
323-
localArr.length &&
324-
prodArr.length
325-
) {
326-
// Auto-detect a shared *_id (or "id") to compare by counts
327-
const idKey = chooseCommonIdKey(localArr, prodArr);
404+
if (
405+
Array.isArray(localArr) &&
406+
Array.isArray(prodArr) &&
407+
localArr.length &&
408+
prodArr.length
409+
) {
410+
// 3a) Sample variance (lenient)
411+
if (/\bsample\b/i.test(requestPath)) {
412+
if (localArr.length === prodArr.length) {
413+
console.warn(
414+
`⚠️ Sample variance for endpoint: ${requestPath}\n` +
415+
` - Dev sample length: ${localArr.length}\n` +
416+
` - Prod sample length: ${prodArr.length}\n` +
417+
` → Status: OK (non-deterministic sample selection)`,
418+
);
419+
return;
420+
}
421+
}
328422

329-
if (idKey) {
330-
console.info(`[compareWithProduction] Using id key: ${idKey}`);
331-
const localIds = uniqueIds(localArr, idKey);
332-
const prodIds = uniqueIds(prodArr, idKey);
333-
const localCount = localIds.size;
334-
const prodCount = prodIds.size;
423+
const idKey = chooseCommonIdKey(localArr, prodArr);
424+
if (idKey) {
425+
const localIds = uniqueIds(localArr, idKey);
426+
const prodIds = uniqueIds(prodArr, idKey);
335427

336-
if (localCount !== prodCount) {
428+
// 3b) Same-id-set but field-level drift (lenient)
429+
if (sameIdSet(localIds, prodIds)) {
430+
const { onlyInLocal, onlyInProd } = diffItemKeys(localArr, prodArr);
431+
if (onlyInLocal.length || onlyInProd.length) {
337432
console.warn(
338433
[
339-
`⚠️ ${idKey} count mismatch for endpoint: ${requestPath}`,
340-
` - Dev (current host) ${idKey} count: ${localCount}`,
341-
` - Prod (host_prod) ${idKey} count: ${prodCount}`,
434+
`⚠️ Field-level drift for endpoint: ${requestPath}`,
435+
` - Same ${idKey} set, but object keys differ`,
436+
` - Only in Dev (current host): ${
437+
onlyInLocal.length ? onlyInLocal.join(", ") : "(none)"
438+
}`,
439+
` - Only in Prod (host_prod): ${
440+
onlyInProd.length ? onlyInProd.join(", ") : "(none)"
441+
}`,
442+
` → Status: OK (field drift tolerated)`,
342443
].join("\n"),
343444
);
344-
345445
return;
346446
}
347-
// If counts match but payloads differ, fall through to strict error to surface real mismatches.
348447
}
349-
// If we couldn't find a shared idKey, we’ll fall back to strict diff below.
448+
449+
// 3c) idKey count mismatch (lenient)
450+
if (localIds.size !== prodIds.size) {
451+
console.warn(
452+
[
453+
`⚠️ ${idKey} count mismatch for endpoint: ${requestPath}`,
454+
` - Dev (current host) ${idKey} count: ${localIds.size}`,
455+
` - Prod (host_prod) ${idKey} count: ${prodIds.size}`,
456+
].join("\n"),
457+
);
458+
return;
459+
}
350460
}
461+
}
351462

352-
// Strict mismatch error with helpful diff
353-
throw new Error(
354-
`Mismatch for endpoint: ${requestPath}\n` +
355-
`Local: ${JSON.stringify(localResponse.body, null, 2)}\n` +
356-
`Production: ${JSON.stringify(prodData, null, 2)}`,
357-
);
358-
},
359-
};
463+
// 4) Strict mismatch
464+
throw new Error(
465+
`Mismatch for endpoint: ${requestPath}\n` +
466+
`Local: ${JSON.stringify(localResponse.body, null, 2)}\n` +
467+
`Production: ${JSON.stringify(prodData, null, 2)}`,
468+
);
469+
}
470+
471+
}

0 commit comments

Comments
 (0)