Skip to content

Commit 139f5fe

Browse files
add unit tests for AssetQuery, Asset, BaseQuery, Cache, ContentType, Entries, Pagination, Query, and Stack classes
- Added tests for debug logging integration in Contentstack.
1 parent 4901c15 commit 139f5fe

10 files changed

Lines changed: 397 additions & 5 deletions

test/unit/asset-query.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ describe('AssetQuery class', () => {
4848
expect(assetQuery._queryParams.include_fallback).toBe('true');
4949
});
5050

51+
it('should add "include_metadata" in queryParameter when includeMetadata method is called', () => {
52+
const returnedValue = assetQuery.includeMetadata();
53+
expect(returnedValue).toBeInstanceOf(AssetQuery);
54+
expect(assetQuery._queryParams.include_metadata).toBe('true');
55+
});
56+
5157
it('should add "locale" in Parameter when locale method is called', () => {
5258
const returnedValue = assetQuery.locale('en-us');
5359
expect(returnedValue).toBeInstanceOf(AssetQuery);

test/unit/asset.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ describe('Asset class', () => {
4242
expect(asset._queryParams.include_fallback).toBe('true');
4343
});
4444

45+
it('should add "include_metadata" in _queryParams when includeMetadata method is called', () => {
46+
const returnedValue = asset.includeMetadata();
47+
expect(returnedValue).toBeInstanceOf(Asset);
48+
expect(asset._queryParams.include_metadata).toBe('true');
49+
});
50+
4551
it('should add "relative_urls" in _queryParams when relativeUrl method is called', () => {
4652
const returnedValue = asset.relativeUrls();
4753
expect(returnedValue).toBeInstanceOf(Asset);
@@ -59,4 +65,13 @@ describe('Asset class', () => {
5965
const returnedValue = await asset.fetch();
6066
expect(returnedValue).toEqual(assetFetchDataMock.asset);
6167
});
68+
69+
it('should return response directly when asset property is not present', async () => {
70+
const responseWithoutAsset = { data: 'test', uid: 'test-uid' };
71+
mockClient.onGet(`/assets/assetUid`).reply(200, responseWithoutAsset);
72+
73+
const result = await asset.fetch();
74+
75+
expect(result).toEqual(responseWithoutAsset);
76+
});
6277
});

test/unit/base-query.spec.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,12 @@ describe('BaseQuery class', () => {
7676
});
7777

7878
class TestableBaseQuery extends BaseQuery {
79-
constructor(client: AxiosInstance, urlPath: string) {
79+
constructor(client: AxiosInstance, urlPath: string | null = null) {
8080
super();
8181
this._client = client;
82-
this._urlPath = urlPath;
82+
if (urlPath !== null) {
83+
this._urlPath = urlPath;
84+
}
8385
this._variants = '';
8486
}
8587

@@ -90,6 +92,10 @@ class TestableBaseQuery extends BaseQuery {
9092
setParameters(params: any) {
9193
this._parameters = params;
9294
}
95+
96+
setUrlPath(path: string) {
97+
this._urlPath = path;
98+
}
9399
}
94100

95101
describe('BaseQuery find method', () => {
@@ -164,4 +170,12 @@ describe('BaseQuery find method', () => {
164170

165171
expect(result).toEqual(entryFindMock);
166172
});
173+
174+
it('should handle empty _urlPath gracefully', () => {
175+
const queryWithoutUrlPath = new TestableBaseQuery(client, null);
176+
queryWithoutUrlPath.setUrlPath('');
177+
178+
// Verify that URL path is empty (testing the null check in extractContentTypeUidFromUrl)
179+
expect(queryWithoutUrlPath).toBeInstanceOf(TestableBaseQuery);
180+
});
167181
});

test/unit/cache.spec.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,4 +329,93 @@ describe("Cache handleRequest function", () => {
329329
cacheStore.removeItem(enhancedCacheKey, config.contentTypeUid);
330330
});
331331
});
332+
333+
describe("Enhanced cache key with entryUid", () => {
334+
it("should extract entryUid from URL pattern", async () => {
335+
const cacheOptions = { policy: Policy.CACHE_THEN_NETWORK, maxAge: 3600 };
336+
const defaultAdapter = jest.fn((_config) => ({
337+
data: JSON.stringify("foo"),
338+
}));
339+
const configWithUrl = {
340+
...config,
341+
url: '/content_types/test_ct/entries/entry123',
342+
};
343+
344+
const cacheStore = new PersistanceStore(cacheOptions);
345+
346+
await handleRequest(
347+
cacheOptions,
348+
apiKey,
349+
defaultAdapter,
350+
resolve,
351+
reject,
352+
configWithUrl
353+
);
354+
355+
expect(defaultAdapter).toHaveBeenCalled();
356+
expect(resolve).toBeCalledWith({ data: "foo" });
357+
358+
// Clean up with enhanced key that includes entry UID
359+
const enhancedCacheKey = `${config.contentTypeUid}_${apiKey}_entry_entry123`;
360+
cacheStore.removeItem(enhancedCacheKey, config.contentTypeUid);
361+
});
362+
363+
it("should use entryUid from config when available", async () => {
364+
const cacheOptions = { policy: Policy.CACHE_THEN_NETWORK, maxAge: 3600 };
365+
const defaultAdapter = jest.fn((_config) => ({
366+
data: JSON.stringify("foo"),
367+
}));
368+
const configWithEntryUid = {
369+
...config,
370+
entryUid: 'entry456',
371+
};
372+
373+
const cacheStore = new PersistanceStore(cacheOptions);
374+
375+
await handleRequest(
376+
cacheOptions,
377+
apiKey,
378+
defaultAdapter,
379+
resolve,
380+
reject,
381+
configWithEntryUid
382+
);
383+
384+
expect(defaultAdapter).toHaveBeenCalled();
385+
expect(resolve).toBeCalledWith({ data: "foo" });
386+
387+
// Clean up with enhanced key that includes entry UID
388+
const enhancedCacheKey = `${config.contentTypeUid}_${apiKey}_entry_entry456`;
389+
cacheStore.removeItem(enhancedCacheKey, config.contentTypeUid);
390+
});
391+
392+
it("should return null when URL does not match entry pattern", async () => {
393+
const cacheOptions = { policy: Policy.CACHE_THEN_NETWORK, maxAge: 3600 };
394+
const defaultAdapter = jest.fn((_config) => ({
395+
data: JSON.stringify("foo"),
396+
}));
397+
const configWithInvalidUrl = {
398+
...config,
399+
url: '/assets',
400+
};
401+
402+
const cacheStore = new PersistanceStore(cacheOptions);
403+
404+
await handleRequest(
405+
cacheOptions,
406+
apiKey,
407+
defaultAdapter,
408+
resolve,
409+
reject,
410+
configWithInvalidUrl
411+
);
412+
413+
expect(defaultAdapter).toHaveBeenCalled();
414+
expect(resolve).toBeCalledWith({ data: "foo" });
415+
416+
// Clean up with standard enhanced key (no entry UID)
417+
const enhancedCacheKey = `${config.contentTypeUid}_${apiKey}`;
418+
cacheStore.removeItem(enhancedCacheKey, config.contentTypeUid);
419+
});
420+
});
332421
});
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { httpClient, AxiosInstance } from '@contentstack/core';
2+
import * as Contentstack from '../../src/lib/contentstack';
3+
import { Stack } from '../../src/lib/stack';
4+
import { Policy, StackConfig } from '../../src/lib/types';
5+
import MockAdapter from 'axios-mock-adapter';
6+
7+
describe('Contentstack Debug Logging Integration', () => {
8+
let mockLogHandler: jest.Mock;
9+
10+
beforeEach(() => {
11+
mockLogHandler = jest.fn();
12+
});
13+
14+
it('should execute debug logging for request interceptor', async () => {
15+
const config: StackConfig = {
16+
apiKey: "apiKey",
17+
deliveryToken: "delivery",
18+
environment: "env",
19+
debug: true,
20+
logHandler: mockLogHandler,
21+
};
22+
23+
const stack = Contentstack.stack(config);
24+
const client = stack.getClient();
25+
const mockClient = new MockAdapter(client);
26+
27+
mockClient.onGet('/content_types/test').reply(200, {
28+
content_types: []
29+
});
30+
31+
// Make an actual request to trigger interceptors
32+
try {
33+
await client.get('/content_types/test');
34+
} catch (e) {
35+
// Ignore errors
36+
}
37+
38+
// Verify request logging was called
39+
const requestLogs = mockLogHandler.mock.calls.filter((call: any) =>
40+
call[1]?.type === 'request'
41+
);
42+
expect(requestLogs.length).toBeGreaterThan(0);
43+
44+
mockClient.restore();
45+
});
46+
47+
it('should execute debug logging for response interceptor with 2xx status', async () => {
48+
const config: StackConfig = {
49+
apiKey: "apiKey",
50+
deliveryToken: "delivery",
51+
environment: "env",
52+
debug: true,
53+
logHandler: mockLogHandler,
54+
};
55+
56+
const stack = Contentstack.stack(config);
57+
const client = stack.getClient();
58+
const mockClient = new MockAdapter(client);
59+
60+
mockClient.onGet('/content_types/test').reply(200, {
61+
content_types: []
62+
});
63+
64+
await client.get('/content_types/test');
65+
66+
// Verify response logging was called with info level
67+
const responseLogs = mockLogHandler.mock.calls.filter((call: any) =>
68+
call[1]?.type === 'response' && call[0] === 'info'
69+
);
70+
expect(responseLogs.length).toBeGreaterThan(0);
71+
72+
mockClient.restore();
73+
});
74+
75+
it('should execute debug logging for response interceptor with 3xx status', async () => {
76+
const config: StackConfig = {
77+
apiKey: "apiKey",
78+
deliveryToken: "delivery",
79+
environment: "env",
80+
debug: true,
81+
logHandler: mockLogHandler,
82+
};
83+
84+
const stack = Contentstack.stack(config);
85+
const client = stack.getClient();
86+
const mockClient = new MockAdapter(client);
87+
88+
// 3xx responses are treated as errors by axios-mock-adapter
89+
mockClient.onGet('/content_types/test').reply(304, {});
90+
91+
try {
92+
await client.get('/content_types/test');
93+
} catch (e) {
94+
// Expected - 3xx responses trigger error handler in mock adapter
95+
}
96+
97+
// Verify error response logging was called - 3xx goes through error interceptor
98+
const errorLogs = mockLogHandler.mock.calls.filter((call: any) =>
99+
call[1]?.type === 'response_error' && call[1]?.status === 304
100+
);
101+
expect(errorLogs.length).toBeGreaterThan(0);
102+
103+
mockClient.restore();
104+
});
105+
106+
it('should execute debug logging for error response interceptor with 4xx status', async () => {
107+
const config: StackConfig = {
108+
apiKey: "apiKey",
109+
deliveryToken: "delivery",
110+
environment: "env",
111+
debug: true,
112+
logHandler: mockLogHandler,
113+
};
114+
115+
const stack = Contentstack.stack(config);
116+
const client = stack.getClient();
117+
const mockClient = new MockAdapter(client);
118+
119+
mockClient.onGet('/content_types/test').reply(404, {
120+
error: 'Not found'
121+
});
122+
123+
try {
124+
await client.get('/content_types/test');
125+
} catch (e) {
126+
// Expected error
127+
}
128+
129+
// Verify error logging was called
130+
const errorLogs = mockLogHandler.mock.calls.filter((call: any) =>
131+
call[1]?.type === 'response_error' && call[0] === 'error'
132+
);
133+
expect(errorLogs.length).toBeGreaterThan(0);
134+
135+
mockClient.restore();
136+
});
137+
138+
it('should execute debug logging for error response without status', async () => {
139+
const config: StackConfig = {
140+
apiKey: "apiKey",
141+
deliveryToken: "delivery",
142+
environment: "env",
143+
debug: true,
144+
logHandler: mockLogHandler,
145+
};
146+
147+
const stack = Contentstack.stack(config);
148+
const client = stack.getClient();
149+
const mockClient = new MockAdapter(client);
150+
151+
mockClient.onGet('/content_types/test').networkError();
152+
153+
try {
154+
await client.get('/content_types/test');
155+
} catch (e) {
156+
// Expected network error
157+
}
158+
159+
// Verify error logging was called with debug level for no status
160+
const errorLogs = mockLogHandler.mock.calls.filter((call: any) =>
161+
call[1]?.type === 'response_error' && call[0] === 'debug'
162+
);
163+
expect(errorLogs.length).toBeGreaterThan(0);
164+
165+
mockClient.restore();
166+
});
167+
168+
it('should execute cache adapter when cacheOptions is provided', async () => {
169+
const config: StackConfig = {
170+
apiKey: "apiKey",
171+
deliveryToken: "delivery",
172+
environment: "env",
173+
cacheOptions: {
174+
policy: Policy.CACHE_THEN_NETWORK,
175+
maxAge: 3600,
176+
},
177+
};
178+
179+
const stack = Contentstack.stack(config);
180+
const client = stack.getClient();
181+
const mockClient = new MockAdapter(client);
182+
183+
mockClient.onGet('/content_types/test').reply(200, {
184+
content_types: []
185+
});
186+
187+
// Make request to trigger cache adapter
188+
await client.get('/content_types/test', { contentTypeUid: 'test' });
189+
190+
expect(client.defaults.adapter).toBeDefined();
191+
192+
mockClient.restore();
193+
});
194+
});
195+

test/unit/contenttype.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,13 @@ describe('ContentType class', () => {
4141
const response = await contentType.fetch();
4242
expect(response).toEqual(contentTypeResponseMock.content_type);
4343
});
44+
45+
it('should return response directly when content_type property is not present', async () => {
46+
const responseWithoutContentType = { data: 'test', uid: 'test-uid' };
47+
mockClient.onGet('/content_types/contentTypeUid').reply(200, responseWithoutContentType);
48+
49+
const result = await contentType.fetch();
50+
51+
expect(result).toEqual(responseWithoutContentType);
52+
});
4453
});

test/unit/entries.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ describe('Entries class', () => {
9191
expect(returnedValue).toBeInstanceOf(Query);
9292
});
9393

94+
it('should return Query instance with queryObj when query method is called with object', () => {
95+
const queryObj = { title: 'Test' };
96+
const returnedValue = entry.query(queryObj);
97+
expect(returnedValue).toBeInstanceOf(Query);
98+
expect(returnedValue._parameters).toEqual(queryObj);
99+
});
100+
94101
it('should add a fieldUid to the _queryParams object', () => {
95102
entry.only('fieldUid');
96103
expect(entry._queryParams).toEqual({ 'only[BASE][]': 'fieldUid' });

0 commit comments

Comments
 (0)