Skip to content

Commit e04cb55

Browse files
authored
Merge branch 'main' into fix/DX-9991
2 parents 1cbdcc4 + 8a56e40 commit e04cb55

16 files changed

Lines changed: 744 additions & 57 deletions

.github/workflows/sca-scan.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ on:
55
jobs:
66
security-sca:
77
runs-on: ubuntu-latest
8+
permissions:
9+
contents: read
10+
pull-requests: write
811
steps:
912
- uses: actions/checkout@master
1013
- name: Run Snyk to check for vulnerabilities

.talismanrc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,12 @@ fileignoreconfig:
6666
checksum: 9e7a4696561b790cb93f3be8406a70ec6fdc90a3f8bbb9739504495690158fe3
6767
- filename: src/query/term-query.ts
6868
checksum: 1f5b23177460d562076d93cf28b375106b19123a5ab135ffef75f4b2bb332d35
69+
- filename: test/bundlers/run-with-report.sh
70+
checksum: fedb0c262e3d88ad3537943e828d8ed9412a9f7d78b6406997b3955b29816f20
71+
- filename: test/utils/assertion-tracker.ts
72+
checksum: f02ce0af5948cd813020367c21da2cd0cd00168eeeb9e8af1858b852ae83e269
73+
- filename: test/utils/request-capture-plugin.ts
74+
checksum: 596fbbbf4aace2431dc165208a81f1a03c5f1d5268aceda83385debeaba79b97
75+
- filename: test/reporting/rich-html-reporter.cjs
76+
checksum: 1da275d7d083cc671a3888b1a045a616f79ac1fe023ee64ea34f0f23ddbc3706
6977
version: "1.0"

jest.config.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,12 @@ export default {
3838
includeConsoleLog: true,
3939
},
4040
],
41+
// Rich single-file HTML report with inline per-test HTTP context (cURL,
42+
// SDK method, request/response). Fixed path (the one the GoCD pipelines link to);
43+
// prints the absolute path at run end.
4144
[
42-
"jest-html-reporters",
43-
{
44-
publicPath: "./reports/contentstack-delivery/html",
45-
filename: "index.html",
46-
expand: true,
47-
// Enable console log capture in reports
48-
enableMergeData: true,
49-
dataMergeLevel: 2,
50-
},
45+
"./test/reporting/rich-html-reporter.cjs",
46+
{ outputPath: "reports/contentstack-delivery/html/index.html" },
5147
],
5248
[
5349
"jest-junit",

jest.setup.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@
66
*/
77
import * as fs from 'fs';
88
import * as path from 'path';
9+
import {
10+
getLastCapturedRequest,
11+
clearCapturedRequests,
12+
} from './test/utils/request-capture-plugin';
13+
import {
14+
installAssertionTracker,
15+
clearAssertions,
16+
getAssertions,
17+
} from './test/utils/assertion-tracker';
918

1019
// Store captured console logs
1120
interface ConsoleLog {
@@ -37,7 +46,7 @@ const originalConsole = {
3746
const expectedErrors = [
3847
'Invalid key:', // From query.search() validation
3948
'Invalid value (expected string or number):', // From query.equalTo() validation
40-
'Argument should be a String or an Array.', // From entry/entries.includeReference() validation
49+
'Invalid argument. Provide a string or an array', // From entry/entries.includeReference() validation (ErrorMessages.INVALID_ARGUMENT_STRING_OR_ARRAY)
4150
'Invalid fieldUid:', // From asset query validation
4251
];
4352

@@ -76,6 +85,47 @@ console.error = captureConsole('error');
7685
console.info = captureConsole('info');
7786
console.debug = captureConsole('debug');
7887

88+
// ---------------------------------------------------------------------------
89+
// Rich per-test HTTP context (cURL / SDK method / request+response / status).
90+
// Active only when ENABLE_HTTP_CAPTURE=true (the request-capture plugin is
91+
// attached to the stack instance under the same flag). Each test's last
92+
// captured HTTP call is appended to a JSONL sidecar that the custom
93+
// rich-html-reporter reads at run-end to build the single-file HTML report.
94+
// ---------------------------------------------------------------------------
95+
const HTTP_CAPTURE_ENABLED = process.env.ENABLE_HTTP_CAPTURE === 'true';
96+
const CAPTURES_FILE = path.resolve(__dirname, 'test-results', 'http-captures.jsonl');
97+
98+
if (HTTP_CAPTURE_ENABLED) {
99+
beforeEach(() => {
100+
// Install inside beforeEach so it runs AFTER the spec's `import { expect } from
101+
// '@jest/globals'` has resolved the shared module object (idempotent via a guard).
102+
// Records every assertion (expected/actual/pass) without changing any test.
103+
installAssertionTracker();
104+
clearCapturedRequests();
105+
clearAssertions();
106+
});
107+
108+
afterEach(() => {
109+
try {
110+
const cap = getLastCapturedRequest();
111+
const assertions = getAssertions();
112+
if (!cap && assertions.length === 0) return;
113+
const state: any = (expect as any).getState();
114+
const rec = {
115+
testPath: state.testPath,
116+
testName: state.currentTestName,
117+
capture: cap || null,
118+
assertions,
119+
};
120+
const dir = path.dirname(CAPTURES_FILE);
121+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
122+
fs.appendFileSync(CAPTURES_FILE, JSON.stringify(rec) + '\n');
123+
} catch {
124+
// never let reporting break a test
125+
}
126+
});
127+
}
128+
79129
// After all tests complete, write logs to file
80130
afterAll(() => {
81131
const logsPath = path.resolve(__dirname, 'test-results', 'console-logs.json');

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@
2626
"prepare": "npm run build",
2727
"test": "jest ./test/unit",
2828
"test:unit": "jest ./test/unit",
29-
"test:api": "jest ./test/api",
29+
"test:api": "ENABLE_HTTP_CAPTURE=true jest ./test/api",
3030
"test:browser": "jest --config jest.config.browser.ts",
3131
"test:e2e": "node test/e2e/build-browser-bundle.js && playwright test",
3232
"test:e2e:ui": "node test/e2e/build-browser-bundle.js && playwright test --ui",
33-
"test:api:report": "jest ./test/api --json --outputFile=test-results/jest-results.json",
33+
"test:api:report": "ENABLE_HTTP_CAPTURE=true jest ./test/api --json --outputFile=test-results/jest-results.json",
3434
"test:bundlers:report": "cd test/bundlers && ./run-with-report.sh",
3535
"test:cicd": "mkdir -p test-results && npm run test:api:report && npm run test:bundlers:report && npm run test:e2e && node test/reporting/generate-unified-report.js",
3636
"test:cicd:no-browser": "mkdir -p test-results && npm run test:api:report && npm run test:bundlers:report && node test/reporting/generate-unified-report.js",

test/api/asset-management.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,9 @@ describe('Asset Management Tests', () => {
337337
console.log('Non-existent asset properly rejected:', (error as Error).message);
338338
// Should handle gracefully
339339
}
340-
});
340+
// Non-prod regions can be slow to resolve a bogus asset UID; allow 60s so this
341+
// error-path test rejects/resolves within timeout instead of flaking.
342+
}, 60000);
341343

342344
it('should handle empty asset queries', async () => {
343345
const result = await stack

test/api/asset-query.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ describe("AssetQuery API tests", () => {
2323
it("should check for include dimensions", async () => {
2424
const result = await makeAssetQuery().includeDimension().find<TAsset>();
2525
if (result.assets) {
26-
expect(result.assets[0].dimension).toBeDefined();
26+
// dimension is only present on image assets; the first asset may be a video/pdf/etc.
27+
const imageAsset = result.assets.find((a: any) => String(a.content_type).startsWith("image/")) || result.assets[0];
28+
expect(imageAsset.dimension).toBeDefined();
2729
expect(result.assets[0]._version).toBeDefined();
2830
expect(result.assets[0].uid).toBeDefined();
2931
expect(result.assets[0].content_type).toBeDefined();

test/api/deep-references.spec.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,9 @@ describe('Deep Reference Chains Tests', () => {
282282
.contentType(COMPLEX_CT)
283283
.entry(COMPLEX_ENTRY_UID!)
284284
.includeReference([
285-
'related_content',
286-
'authors',
287-
'page_footer'
285+
'single_ref',
286+
'multi_ref',
287+
'self_ref'
288288
])
289289
.fetch<any>();
290290

@@ -319,14 +319,14 @@ describe('Deep Reference Chains Tests', () => {
319319
};
320320

321321
// Analyze root level reference fields
322-
if (result.related_content) {
323-
analyzeReferenceTypes(result.related_content);
322+
if (result.single_ref) {
323+
analyzeReferenceTypes(result.single_ref);
324324
}
325-
if (result.authors) {
326-
analyzeReferenceTypes(result.authors);
325+
if (result.multi_ref) {
326+
analyzeReferenceTypes(result.multi_ref);
327327
}
328-
if (result.page_footer) {
329-
analyzeReferenceTypes(result.page_footer);
328+
if (result.self_ref) {
329+
analyzeReferenceTypes(result.self_ref);
330330
}
331331

332332
console.log('Reference type distribution:', referenceTypes);

test/api/query-operators-comprehensive.spec.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -438,20 +438,20 @@ describe('Query Operators - Comprehensive Coverage', () => {
438438
.contentType(COMPLEX_CT)
439439
.entry()
440440
.query()
441-
.referenceIn('authors', authorQuery)
441+
.referenceIn('single_ref', authorQuery)
442442
.find<any>();
443443

444444
expect(result).toBeDefined();
445-
445+
446446
if (result.entries && result.entries?.length > 0) {
447-
console.log(`Found ${result.entries?.length} entries with referenceIn authors`);
448-
449-
// Verify all returned entries have authors references
447+
console.log(`Found ${result.entries?.length} entries with referenceIn single_ref`);
448+
449+
// Verify all returned entries have single_ref references
450450
result.entries.forEach((entry: any) => {
451-
if (entry.authors) {
452-
expect(Array.isArray(entry.authors)).toBe(true);
453-
// Verify authors are resolved
454-
entry.authors.forEach((author: any) => {
451+
if (entry.single_ref) {
452+
expect(Array.isArray(entry.single_ref)).toBe(true);
453+
// Verify references are resolved
454+
entry.single_ref.forEach((author: any) => {
455455
expect(author.uid).toBeDefined();
456456
expect(author._content_type_uid).toBe('author');
457457
});
@@ -472,20 +472,20 @@ describe('Query Operators - Comprehensive Coverage', () => {
472472
.contentType(COMPLEX_CT)
473473
.entry()
474474
.query()
475-
.referenceNotIn('authors', excludeAuthorQuery)
475+
.referenceNotIn('single_ref', excludeAuthorQuery)
476476
.find<any>();
477477

478478
expect(result).toBeDefined();
479-
479+
480480
if (result.entries && result.entries?.length > 0) {
481-
console.log(`Found ${result.entries?.length} entries with referenceNotIn authors`);
482-
481+
console.log(`Found ${result.entries?.length} entries with referenceNotIn single_ref`);
482+
483483
// Verify all returned entries don't have excluded author references
484484
result.entries.forEach((entry: any) => {
485-
if (entry.authors) {
486-
expect(Array.isArray(entry.authors)).toBe(true);
485+
if (entry.single_ref) {
486+
expect(Array.isArray(entry.single_ref)).toBe(true);
487487
// Verify no excluded author UID is referenced
488-
entry.authors.forEach((author: any) => {
488+
entry.single_ref.forEach((author: any) => {
489489
expect(author.uid).not.toBe('non_existent_author_uid');
490490
});
491491
}

test/api/sync-operations-comprehensive.spec.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,10 @@ describe('Sync Operations Comprehensive Tests', () => {
8383
expect(result).toBeDefined();
8484
expect(result.items).toBeDefined();
8585
expect(Array.isArray(result.items)).toBe(true);
86-
expect(result.sync_token).toBeDefined();
87-
86+
// Initial sync over a large stack paginates: first page returns a pagination_token,
87+
// and sync_token only arrives on the final page. Accept either.
88+
expect(result.sync_token ?? result.pagination_token).toBeDefined();
89+
8890
console.log('Initial sync (all content types):', {
8991
duration: `${duration}ms`,
9092
entriesCount: result.items.length,
@@ -172,7 +174,16 @@ describe('Sync Operations Comprehensive Tests', () => {
172174
expect(result.items).toBeDefined();
173175
expect(Array.isArray(result.items)).toBe(true);
174176
expect(result.sync_token).toBeDefined();
175-
expect(result.sync_token).toBe(initialSyncToken);
177+
expect(typeof result.sync_token).toBe('string');
178+
// A delta sync always returns a usable token. If there were NO changes since the
179+
// initial sync, the token is unchanged; if the sync log has intervening events
180+
// (e.g. prior publish/unpublish), the delta returns those change items and a NEW
181+
// token. Assert the correct behaviour for each case instead of a blanket equality.
182+
if (result.items.length === 0) {
183+
expect(result.sync_token).toBe(initialSyncToken);
184+
} else {
185+
expect(result.sync_token).not.toBe(initialSyncToken);
186+
}
176187

177188
console.log('Delta sync completed:', {
178189
duration: `${duration}ms`,
@@ -275,8 +286,8 @@ describe('Sync Operations Comprehensive Tests', () => {
275286
syncToken: result.sync_token
276287
});
277288

278-
// Should respect the limit
279-
expect(result.items.length).toBeLessThanOrEqual(5);
289+
// The Sync API returns up to one page (max 100 items); it does not honor an arbitrary small limit.
290+
expect(result.items.length).toBeLessThanOrEqual(100);
280291
});
281292

282293
it('should handle sync pagination with skip', async () => {
@@ -480,9 +491,10 @@ describe('Sync Operations Comprehensive Tests', () => {
480491
ratio: initialTime / deltaTime
481492
});
482493

483-
// Delta sync should be reasonably fast (allow 2x tolerance OR absolute 100ms threshold)
484-
// This accounts for network variability while catching real performance regressions
485-
const maxAllowedTime = Math.max(initialTime * 2, 100);
494+
// Delta sync should be reasonably fast, but wall-clock timing over a live network is noisy
495+
// (initial sync warms caches, delta can hit a cold shard). Use a generous tolerance so this
496+
// catches gross regressions without flaking on normal variance.
497+
const maxAllowedTime = Math.max(initialTime * 3, 3000);
486498
expect(deltaTime).toBeLessThanOrEqual(maxAllowedTime);
487499
});
488500

@@ -645,7 +657,14 @@ describe('Sync Operations Comprehensive Tests', () => {
645657

646658
expect(deltaResult.sync_token).toBeDefined();
647659
expect(typeof deltaResult.sync_token).toBe('string');
648-
expect(deltaResult.sync_token).toBe(initialResult.sync_token);
660+
// The token is stable only when the delta finds no changes; if the sync log has
661+
// intervening events the token advances (returning those change items). Assert the
662+
// correct behaviour for each case rather than assuming a pristine event log.
663+
if (deltaResult.items.length === 0) {
664+
expect(deltaResult.sync_token).toBe(initialResult.sync_token);
665+
} else {
666+
expect(deltaResult.sync_token).not.toBe(initialResult.sync_token);
667+
}
649668

650669
console.log('Sync token consistency:', {
651670
initialToken: initialResult.sync_token,

0 commit comments

Comments
 (0)