Skip to content

Commit 42cdbf1

Browse files
committed
Sanity tests: dynamic setup, report context, fixes, security cleanup
- Dynamic stack/token setup; Expected vs Actual + cURL in Mochawesome reports - ContentstackClient: use instrumented client by default, new client when authtoken passed - Fix token validation assertions, audit log expected status, MEMBER_EMAIL usage - Security: replace blt UIDs and testcs@contentstack.com with placeholders - Update .talismanrc checksums for modified sanity test files
1 parent 9f18d9d commit 42cdbf1

File tree

12 files changed

+769
-182
lines changed

12 files changed

+769
-182
lines changed

.talismanrc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ fileignoreconfig:
4242
- filename: test/sanity-check/mock/global-fields.js
4343
checksum: fb89a4a5028066689de774ca2f990c25c8a3acc46c0c6b97fee410f491853cc1
4444
- filename: test/sanity-check/utility/ContentstackClient.js
45-
checksum: 24d00c8994e7a9986a83e7caafd80c55138ea9d582dc31c7bb7c650fa712bfc0
45+
checksum: 8ad5bf958e40cb65181dec35842e2e292f51cca0f7ca1e87c67cb58cd16f139d
4646
- filename: test/sanity-check/api/variantGroup-test.js
4747
checksum: 3fc26eca704bc9ce4650056c81be45f3586d3c947a18dfec58fee4447de56360
4848
- filename: test/sanity-check/api/workflow-test.js
@@ -52,7 +52,7 @@ fileignoreconfig:
5252
- filename: test/sanity-check/mock/content-types/index.js
5353
checksum: ff47f74037e22f791e2d7c6afbaccf7857b26b51dd2e2361b5b4b70d36057b7f
5454
- filename: test/sanity-check/sanity.js
55-
checksum: c64975a9058c2d780ba725a1e40c037440f830a537849d3a6324ad934454b2ab
55+
checksum: 94fc68fc78e00b8b268f6e86b5ed55dbfe48fbde45f780629afa1c75c968f438
5656
- filename: test/sanity-check/api/user-test.js
5757
checksum: 5f1284561725f99980a800c87d80d2f7b6f56e1efa618adb10bbf87312b0deec
5858
- filename: test/sanity-check/api/locale-test.js
@@ -78,7 +78,7 @@ fileignoreconfig:
7878
- filename: test/sanity-check/api/branchAlias-test.js
7979
checksum: 0b6cacee74d7636e84ce095198f0234d491b79ea20d3978a742a5495692bd61d
8080
- filename: test/sanity-check/utility/testSetup.js
81-
checksum: 23841aa0365dc059e84311887b2a086e7e8b44c457a98b362649aae61a806a5f
81+
checksum: caa1fa9867a49bb8a458bab5bbc3cdeaf2f4a44d0f1a21e997db237553ea33ab
8282
- filename: test/sanity-check/api/branch-test.js
8383
checksum: 49c8fd18c59d45e4335f766591711849722206bce34860efa8eced7172f44efa
8484
- filename: test/sanity-check/api/stack-test.js

lib/organization/teams/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function Teams (http, data) {
3838
* email: 'abc@abc.com'
3939
* }
4040
* ],
41-
* organizationRole: 'blt09e5dfced326aaea',
41+
* organizationRole: 'blt0000000000000000',
4242
* stackRoleMapping: []
4343
* }
4444
* client.organization('organizationUid').teams('teamUid').update(updateData)

test/sanity-check/api/auditlog-test.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,9 @@ describe('Audit Log API Tests', () => {
123123
await stack.auditLog('nonexistent_log_12345').fetch()
124124
expect.fail('Should have thrown an error')
125125
} catch (error) {
126-
// 422 is also a valid response for invalid UID format
127-
expect(error.status).to.be.oneOf([400, 404, 422])
126+
// API may return 401 (unauthorized), 404 (not found), 422 (invalid UID), or 400
127+
const status = error.status ?? error.response?.status
128+
expect(status, 'Expected 400/401/404/422 for non-existent audit log').to.be.oneOf([400, 401, 404, 422])
128129
}
129130
})
130131

test/sanity-check/api/stack-test.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,10 @@ describe('Stack API Tests', () => {
244244
describe('Stack Share Operations', () => {
245245

246246
it('should share stack with user (requires valid email)', async () => {
247-
// Use SHARE_EMAIL or MEMBER_EMAIL from env
248-
const shareEmail = process.env.SHARE_EMAIL || process.env.MEMBER_EMAIL
247+
const shareEmail = process.env.MEMBER_EMAIL
249248

250249
if (!shareEmail) {
251-
console.log('Skipping stack share - no SHARE_EMAIL or MEMBER_EMAIL provided')
250+
console.log('Skipping stack share - no MEMBER_EMAIL provided')
252251
return
253252
}
254253

test/sanity-check/api/user-test.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,8 @@ describe('User & Authentication API Tests', () => {
210210
expect.fail('Should have thrown an error')
211211
} catch (error) {
212212
expect(error).to.exist
213-
expect(error.status).to.be.oneOf([401, 403])
213+
const status = error.status ?? error.response?.status
214+
expect(status, 'Expected 401/403 in error.status or error.response.status').to.be.oneOf([401, 403])
214215
}
215216
})
216217

@@ -225,7 +226,8 @@ describe('User & Authentication API Tests', () => {
225226
expect.fail('Should have thrown an error')
226227
} catch (error) {
227228
expect(error).to.exist
228-
expect(error.status).to.be.oneOf([401, 403])
229+
const status = error.status ?? error.response?.status
230+
expect(status, 'Expected 401/403 in error.status or error.response.status').to.be.oneOf([401, 403])
229231
}
230232
})
231233
})
@@ -320,7 +322,8 @@ describe('User & Authentication API Tests', () => {
320322
// Some APIs might not error on unauthenticated logout
321323
} catch (error) {
322324
expect(error).to.exist
323-
expect(error.status).to.be.oneOf([401, 403])
325+
const status = error.status ?? error.response?.status
326+
expect(status).to.be.oneOf([401, 403])
324327
}
325328
})
326329

test/sanity-check/sanity.js

Lines changed: 107 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,44 @@
33
*
44
* This file orchestrates all API test suites for the CMA JavaScript SDK.
55
*
6-
* The test suite:
6+
* The test suite is FULLY SELF-CONTAINED and dynamically creates:
77
* 1. Logs in using EMAIL/PASSWORD to get authtoken
8-
* 2. Uses existing test stack from API_KEY
9-
* 3. Runs all API tests against the stack
10-
* 4. Cleans up all created resources (keeps stack empty for next run)
11-
* 5. Logs out
8+
* 2. Creates a NEW test stack (no pre-existing stack required)
9+
* 3. Creates a Management Token for the stack
10+
* 4. Creates a Personalize Project linked to the stack
11+
* 5. Runs all API tests against the stack
12+
* 6. Cleans up all created resources within the stack
13+
* 7. Conditionally deletes stack and personalize project (based on env flag)
14+
* 8. Logs out
1215
*
1316
* Environment Variables Required:
1417
* - EMAIL: User email for login
1518
* - PASSWORD: User password for login
1619
* - HOST: API host URL (e.g., api.contentstack.io, eu-api.contentstack.com)
17-
* - API_KEY: Existing test stack API key
18-
* - ORGANIZATION: Organization UID (for Teams tests)
20+
* - ORGANIZATION: Organization UID (for stack creation and personalize)
1921
*
2022
* Optional:
21-
* - PERSONALIZE_PROJECT_UID: For Variants/Personalize tests
23+
* - PERSONALIZE_HOST: Personalize API host (default: personalize-api.contentstack.com)
24+
* - DELETE_DYNAMIC_RESOURCES: Toggle for deleting stack/personalize (default: true)
25+
* Set to 'false' to preserve resources for debugging
2226
* - MEMBER_EMAIL: For team member operations
2327
* - CLIENT_ID: OAuth client ID
2428
* - APP_ID: OAuth app ID
2529
* - REDIRECT_URI: OAuth redirect URI
2630
*
31+
* NO LONGER REQUIRED (dynamically created):
32+
* - API_KEY: Generated when test stack is created
33+
* - MANAGEMENT_TOKEN: Generated for the test stack
34+
* - PERSONALIZE_PROJECT_UID: Generated when personalize project is created
35+
*
2736
* Usage:
2837
* npm run test:sanity
2938
*
3039
* Or run individual test files:
3140
* npm run test -- --grep "Content Type API Tests"
41+
*
42+
* To preserve resources for debugging:
43+
* DELETE_DYNAMIC_RESOURCES=false npm run test:sanity
3244
*/
3345

3446
import dotenv from 'dotenv'
@@ -76,8 +88,12 @@ before(async function () {
7688
console.error(' EMAIL=your-email@example.com')
7789
console.error(' PASSWORD=your-password')
7890
console.error(' HOST=api.contentstack.io')
79-
console.error(' API_KEY=your-stack-api-key')
8091
console.error(' ORGANIZATION=your-org-uid')
92+
console.error('\nOptional settings:')
93+
console.error(' PERSONALIZE_HOST=personalize-api.contentstack.com')
94+
console.error(' DELETE_DYNAMIC_RESOURCES=true (set to false to preserve for debugging)')
95+
console.error('\nNote: API_KEY, MANAGEMENT_TOKEN, and PERSONALIZE_PROJECT_UID')
96+
console.error('are now dynamically created and no longer required in .env')
8197
throw error
8298
}
8399
})
@@ -88,6 +104,9 @@ before(async function () {
88104

89105
// Clear request log and assertion tracker before each test
90106
beforeEach(function() {
107+
// Clear SDK plugin request capture
108+
testSetup.clearCapturedRequests()
109+
91110
try {
92111
requestLogger.clearRequestLog()
93112
} catch (e) {
@@ -136,12 +155,14 @@ afterEach(function() {
136155
}
137156
}
138157

139-
// For passed tests, try to get the last request from the request logger
140-
let lastRequest = null
141-
try {
142-
lastRequest = requestLogger.getLastRequest()
143-
} catch (e) {
144-
// Request logger might not be active
158+
// Get the last request from SDK plugin capture or fallback to request logger
159+
let lastRequest = testSetup.getLastCapturedRequest()
160+
if (!lastRequest) {
161+
try {
162+
lastRequest = requestLogger.getLastRequest()
163+
} catch (e) {
164+
// Request logger might not be active
165+
}
145166
}
146167

147168
// Add context to Mochawesome report
@@ -156,14 +177,26 @@ afterEach(function() {
156177
value: 'PASSED'
157178
})
158179

159-
// Add assertion details for passed tests (if any tracked via trackedExpect)
180+
// Add assertion details for passed tests (trackedExpect or API result)
160181
if (trackedAssertions.length > 0) {
161182
addContext(this, {
162183
title: '📊 Assertions Verified (Expected vs Actual)',
163184
value: trackedAssertions.map(a =>
164185
`✓ ${a.description}\n Expected: ${a.expected}\n Actual: ${a.actual}`
165186
).join('\n\n')
166187
})
188+
} else if (lastRequest) {
189+
// Fallback: show API result for tests that use expect() not trackedExpect
190+
addContext(this, {
191+
title: '📊 Result (Expected vs Actual)',
192+
value: `Expected: Successful API response\nActual: ${lastRequest.status || 'OK'} - ${lastRequest.method} ${lastRequest.url}`
193+
})
194+
} else {
195+
// Final fallback: test passed but no request/assertion captured
196+
addContext(this, {
197+
title: '📊 Result (Expected vs Actual)',
198+
value: 'Expected: Success\nActual: Test passed (no SDK request captured for this test)'
199+
})
167200
}
168201

169202
// For passed tests, add the last request curl if available
@@ -204,7 +237,35 @@ afterEach(function() {
204237
value: 'FAILED'
205238
})
206239

207-
// Add assertion details for failed tests
240+
// Add Expected vs Actual for failed tests
241+
if (error) {
242+
if (error.expected !== undefined || error.actual !== undefined) {
243+
// Chai assertion error
244+
addContext(this, {
245+
title: '❌ Expected vs Actual',
246+
value: `Expected: ${JSON.stringify(error.expected)}\nActual: ${JSON.stringify(error.actual)}`
247+
})
248+
} else if (error.status || error.errorMessage || apiInfo) {
249+
// API/SDK error (e.g. 422 from API)
250+
const status = error.status ?? apiInfo?.status ?? error.response?.status
251+
const msg = error.errorMessage ?? apiInfo?.errorMessage ?? error.message ?? 'Error'
252+
const errDetails = error.errors || apiInfo?.errors || {}
253+
const detailsStr = Object.keys(errDetails).length ? `\nDetails: ${JSON.stringify(errDetails)}` : ''
254+
addContext(this, {
255+
title: '❌ Expected vs Actual',
256+
value: `Expected: Success\nActual: ${status} - ${msg}${detailsStr}`
257+
})
258+
} else {
259+
// Fallback: any other error (e.g. thrown Error, assertion in test code)
260+
const msg = error.message || String(error)
261+
addContext(this, {
262+
title: '❌ Expected vs Actual',
263+
value: `Expected: Success\nActual: ${msg}`
264+
})
265+
}
266+
}
267+
268+
// Add assertion details for failed tests (from trackedExpect)
208269
if (trackedAssertions.length > 0) {
209270
const passedAssertions = trackedAssertions.filter(a => a.passed)
210271
const failedAssertion = trackedAssertions.find(a => !a.passed)
@@ -225,37 +286,43 @@ afterEach(function() {
225286
})
226287
}
227288
}
289+
290+
// Add cURL from captured request (for ALL failed tests - from SDK plugin)
291+
if (lastRequest && lastRequest.curl) {
292+
addContext(this, {
293+
title: '📋 cURL Command (copy-paste ready)',
294+
value: lastRequest.curl
295+
})
296+
addContext(this, {
297+
title: '📡 API Request',
298+
value: `${lastRequest.method} ${lastRequest.url} [${lastRequest.status || 'N/A'}]`
299+
})
300+
if (lastRequest.sdkMethod && !lastRequest.sdkMethod.startsWith('Unknown')) {
301+
addContext(this, {
302+
title: '📦 SDK Method Tested',
303+
value: lastRequest.sdkMethod
304+
})
305+
}
306+
}
228307
}
229308

230-
// Add API details if available (for failed tests)
309+
// Add API error details if available (for failed tests with API error in response)
231310
if (apiInfo) {
232311
const curl = errorToCurl(apiInfo)
233312

234-
// Try to get SDK method from the last request
235-
const failedSdkMethod = lastRequest?.sdkMethod
236-
237-
// Store for final report
238313
testCurls.push({
239314
test: testTitle,
240315
state: testState,
241-
curl: curl,
242-
sdkMethod: failedSdkMethod,
316+
curl: curl || (lastRequest?.curl),
317+
sdkMethod: lastRequest?.sdkMethod,
243318
details: {
244319
status: apiInfo.status,
245320
message: apiInfo.errorMessage || apiInfo.message,
246321
errors: apiInfo.errors
247322
}
248323
})
249324

250-
// Add SDK Method being tested (for failed tests)
251-
if (failedSdkMethod && !failedSdkMethod.startsWith('Unknown')) {
252-
addContext(this, {
253-
title: '📦 SDK Method Tested',
254-
value: failedSdkMethod
255-
})
256-
}
257-
258-
// Add error/response details
325+
// Add error/response details (skip cURL if already added from lastRequest)
259326
addContext(this, {
260327
title: '❌ API Error Details',
261328
value: {
@@ -267,13 +334,14 @@ afterEach(function() {
267334
}
268335
})
269336

270-
// Add cURL command
271-
addContext(this, {
272-
title: '📋 cURL Command (copy-paste ready)',
273-
value: curl
274-
})
337+
// Add cURL from apiInfo only if we didn't already add from lastRequest
338+
if (!lastRequest?.curl && curl) {
339+
addContext(this, {
340+
title: '📋 cURL Command (copy-paste ready)',
341+
value: curl
342+
})
343+
}
275344

276-
// Add request URL for quick reference
277345
if (apiInfo.request && apiInfo.request.url) {
278346
addContext(this, {
279347
title: '🔗 Request',

test/sanity-check/utility/ContentstackClient.js

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,32 +23,33 @@ import { testContext } from './testSetup.js'
2323

2424
/**
2525
* Create a Contentstack client instance
26+
* Uses testSetup's instrumented client (with request capture plugin) when available.
2627
*
2728
* @param {string|null} authtoken - Optional authtoken (uses testSetup context if not provided)
2829
* @returns {Object} Contentstack client instance
2930
*/
3031
export function contentstackClient(authtoken = null) {
31-
const host = process.env.HOST || 'api.contentstack.io'
32-
33-
// If testContext is available and initialized, use its context
34-
if (testContext && testContext.authtoken && !authtoken) {
35-
return contentstack.client({
36-
host: host,
37-
authtoken: testContext.authtoken,
38-
timeout: 60000
39-
})
32+
// When explicit authtoken is passed (e.g. for error testing), create new client - don't use shared
33+
if (authtoken != null) {
34+
const host = process.env.HOST || 'api.contentstack.io'
35+
return contentstack.client({ host, authtoken, timeout: 60000 })
36+
}
37+
// Use testSetup's client when available - it has the request capture plugin for cURL in reports
38+
if (testContext && testContext.client) {
39+
return testContext.client
4040
}
4141

42-
// Standalone mode with provided authtoken
42+
// Fallback when testSetup not initialized (e.g. unit tests)
43+
const host = process.env.HOST || 'api.contentstack.io'
4344
const params = {
4445
host: host,
4546
timeout: 60000
4647
}
47-
48-
if (authtoken) {
48+
if (testContext?.authtoken && !authtoken) {
49+
params.authtoken = testContext.authtoken
50+
} else if (authtoken) {
4951
params.authtoken = authtoken
5052
}
51-
5253
return contentstack.client(params)
5354
}
5455

0 commit comments

Comments
 (0)