Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/coverage-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: 'TS SDK - Unit Testing'

on:
pull_request:
branches:
- development
- staging
- main

jobs:
coverage:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
checks: write

steps:
- uses: actions/checkout@v4

- name: Run unit tests with coverage
uses: ArtiomTr/jest-coverage-report-action@v2
id: coverage
with:
test-script: npm run test:unit
threshold: 95
9 changes: 6 additions & 3 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ export default {
coverageDirectory: "./reports/contentstack-delivery/coverage/",
collectCoverageFrom: ["src/**", "!src/index.ts"],
coverageThreshold: {
// global: {
// branches: 95,
// }
global: {
statements: 95,
branches: 95,
functions: 95,
lines: 95,
}
},
reporters: [
"default",
Expand Down
6 changes: 6 additions & 0 deletions test/unit/asset-query.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ describe('AssetQuery class', () => {
expect(assetQuery._queryParams.include_fallback).toBe('true');
});

it('should add "include_metadata" in queryParameter when includeMetadata method is called', () => {
const returnedValue = assetQuery.includeMetadata();
expect(returnedValue).toBeInstanceOf(AssetQuery);
expect(assetQuery._queryParams.include_metadata).toBe('true');
});

it('should add "locale" in Parameter when locale method is called', () => {
const returnedValue = assetQuery.locale('en-us');
expect(returnedValue).toBeInstanceOf(AssetQuery);
Expand Down
15 changes: 15 additions & 0 deletions test/unit/asset.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ describe('Asset class', () => {
expect(asset._queryParams.include_fallback).toBe('true');
});

it('should add "include_metadata" in _queryParams when includeMetadata method is called', () => {
const returnedValue = asset.includeMetadata();
expect(returnedValue).toBeInstanceOf(Asset);
expect(asset._queryParams.include_metadata).toBe('true');
});

it('should add "relative_urls" in _queryParams when relativeUrl method is called', () => {
const returnedValue = asset.relativeUrls();
expect(returnedValue).toBeInstanceOf(Asset);
Expand All @@ -59,4 +65,13 @@ describe('Asset class', () => {
const returnedValue = await asset.fetch();
expect(returnedValue).toEqual(assetFetchDataMock.asset);
});

it('should return response directly when asset property is not present', async () => {
const responseWithoutAsset = { data: 'test', uid: 'test-uid' };
mockClient.onGet(`/assets/assetUid`).reply(200, responseWithoutAsset);

const result = await asset.fetch();

expect(result).toEqual(responseWithoutAsset);
});
});
109 changes: 109 additions & 0 deletions test/unit/base-query.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { BaseQuery } from '../../src/lib/base-query';
import { httpClient, AxiosInstance } from '@contentstack/core';
import { MOCK_CLIENT_OPTIONS } from '../utils/constant';
import MockAdapter from 'axios-mock-adapter';
import { entryFindMock } from '../utils/mocks';

describe('BaseQuery class', () => {
let baseQuery: BaseQuery;
Expand Down Expand Up @@ -69,4 +73,109 @@ describe('BaseQuery class', () => {
baseQuery.removeParam('key2');
expect(baseQuery._queryParams).toEqual({ key1: 'value1' });
});
});

class TestableBaseQuery extends BaseQuery {
constructor(client: AxiosInstance, urlPath: string | null = null) {
super();
this._client = client;
if (urlPath !== null) {
this._urlPath = urlPath;
}
this._variants = '';
}

setVariants(variants: string) {
this._variants = variants;
}

setParameters(params: any) {
this._parameters = params;
}

setUrlPath(path: string) {
this._urlPath = path;
}
}

describe('BaseQuery find method', () => {
let client: AxiosInstance;
let mockClient: MockAdapter;
let query: TestableBaseQuery;

beforeAll(() => {
client = httpClient(MOCK_CLIENT_OPTIONS);
mockClient = new MockAdapter(client as any);
});

beforeEach(() => {
query = new TestableBaseQuery(client, '/content_types/test_uid/entries');
mockClient.reset();
});

it('should call find with encode parameter true', async () => {
mockClient.onGet('/content_types/test_uid/entries').reply(200, entryFindMock);

query.setParameters({ title: 'Test' });
const result = await query.find(true);

expect(result).toEqual(entryFindMock);
});

it('should call find without parameters', async () => {
mockClient.onGet('/content_types/test_uid/entries').reply(200, entryFindMock);

const result = await query.find();

expect(result).toEqual(entryFindMock);
});

it('should call find with variants header when variants are set', async () => {
mockClient.onGet('/content_types/test_uid/entries').reply((config) => {
expect(config.headers?.['x-cs-variant-uid']).toBe('variant1,variant2');
return [200, entryFindMock];
});

query.setVariants('variant1,variant2');
await query.find();
});

it('should extract content type UID from URL path', async () => {
mockClient.onGet('/content_types/my_content_type/entries').reply(200, entryFindMock);

const queryWithContentType = new TestableBaseQuery(client, '/content_types/my_content_type/entries');
const result = await queryWithContentType.find();

expect(result).toEqual(entryFindMock);
});

it('should return null for content type UID when URL does not match pattern', async () => {
mockClient.onGet('/assets').reply(200, entryFindMock);

const queryWithoutContentType = new TestableBaseQuery(client, '/assets');
const result = await queryWithoutContentType.find();

expect(result).toEqual(entryFindMock);
});

it('should handle find with both encode and variants', async () => {
mockClient.onGet('/content_types/test_uid/entries').reply((config) => {
expect(config.headers?.['x-cs-variant-uid']).toBe('test-variant');
return [200, entryFindMock];
});

query.setVariants('test-variant');
query.setParameters({ status: 'published' });
const result = await query.find(true);

expect(result).toEqual(entryFindMock);
});

it('should handle empty _urlPath gracefully', () => {
const queryWithoutUrlPath = new TestableBaseQuery(client, null);
queryWithoutUrlPath.setUrlPath('');

// Verify that URL path is empty (testing the null check in extractContentTypeUidFromUrl)
expect(queryWithoutUrlPath).toBeInstanceOf(TestableBaseQuery);
});
});
89 changes: 89 additions & 0 deletions test/unit/cache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,93 @@ describe("Cache handleRequest function", () => {
cacheStore.removeItem(enhancedCacheKey, config.contentTypeUid);
});
});

describe("Enhanced cache key with entryUid", () => {
it("should extract entryUid from URL pattern", async () => {
const cacheOptions = { policy: Policy.CACHE_THEN_NETWORK, maxAge: 3600 };
const defaultAdapter = jest.fn((_config) => ({
data: JSON.stringify("foo"),
}));
const configWithUrl = {
...config,
url: '/content_types/test_ct/entries/entry123',
};

const cacheStore = new PersistanceStore(cacheOptions);

await handleRequest(
cacheOptions,
apiKey,
defaultAdapter,
resolve,
reject,
configWithUrl
);

expect(defaultAdapter).toHaveBeenCalled();
expect(resolve).toBeCalledWith({ data: "foo" });

// Clean up with enhanced key that includes entry UID
const enhancedCacheKey = `${config.contentTypeUid}_${apiKey}_entry_entry123`;
cacheStore.removeItem(enhancedCacheKey, config.contentTypeUid);
});

it("should use entryUid from config when available", async () => {
const cacheOptions = { policy: Policy.CACHE_THEN_NETWORK, maxAge: 3600 };
const defaultAdapter = jest.fn((_config) => ({
data: JSON.stringify("foo"),
}));
const configWithEntryUid = {
...config,
entryUid: 'entry456',
};

const cacheStore = new PersistanceStore(cacheOptions);

await handleRequest(
cacheOptions,
apiKey,
defaultAdapter,
resolve,
reject,
configWithEntryUid
);

expect(defaultAdapter).toHaveBeenCalled();
expect(resolve).toBeCalledWith({ data: "foo" });

// Clean up with enhanced key that includes entry UID
const enhancedCacheKey = `${config.contentTypeUid}_${apiKey}_entry_entry456`;
cacheStore.removeItem(enhancedCacheKey, config.contentTypeUid);
});

it("should return null when URL does not match entry pattern", async () => {
const cacheOptions = { policy: Policy.CACHE_THEN_NETWORK, maxAge: 3600 };
const defaultAdapter = jest.fn((_config) => ({
data: JSON.stringify("foo"),
}));
const configWithInvalidUrl = {
...config,
url: '/assets',
};

const cacheStore = new PersistanceStore(cacheOptions);

await handleRequest(
cacheOptions,
apiKey,
defaultAdapter,
resolve,
reject,
configWithInvalidUrl
);

expect(defaultAdapter).toHaveBeenCalled();
expect(resolve).toBeCalledWith({ data: "foo" });

// Clean up with standard enhanced key (no entry UID)
const enhancedCacheKey = `${config.contentTypeUid}_${apiKey}`;
cacheStore.removeItem(enhancedCacheKey, config.contentTypeUid);
});
});
});
Loading