diff --git a/.github/workflows/coverage-check.yml b/.github/workflows/coverage-check.yml new file mode 100644 index 0000000..6cf518b --- /dev/null +++ b/.github/workflows/coverage-check.yml @@ -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 \ No newline at end of file diff --git a/test/unit/asset-query.spec.ts b/test/unit/asset-query.spec.ts index 7f290c3..de314bb 100644 --- a/test/unit/asset-query.spec.ts +++ b/test/unit/asset-query.spec.ts @@ -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); diff --git a/test/unit/asset.spec.ts b/test/unit/asset.spec.ts index 7fcedcb..f8be0b7 100644 --- a/test/unit/asset.spec.ts +++ b/test/unit/asset.spec.ts @@ -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); @@ -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); + }); }); diff --git a/test/unit/base-query.spec.ts b/test/unit/base-query.spec.ts index 1a2da5c..6f8b943 100644 --- a/test/unit/base-query.spec.ts +++ b/test/unit/base-query.spec.ts @@ -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; @@ -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); + }); }); \ No newline at end of file diff --git a/test/unit/cache.spec.ts b/test/unit/cache.spec.ts index 3e42f9c..1ae933d 100644 --- a/test/unit/cache.spec.ts +++ b/test/unit/cache.spec.ts @@ -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); + }); + }); }); diff --git a/test/unit/contentstack-debug-integration.spec.ts b/test/unit/contentstack-debug-integration.spec.ts new file mode 100644 index 0000000..0187037 --- /dev/null +++ b/test/unit/contentstack-debug-integration.spec.ts @@ -0,0 +1,226 @@ +import { httpClient, AxiosInstance } from '@contentstack/core'; +import * as Contentstack from '../../src/lib/contentstack'; +import { Stack } from '../../src/lib/stack'; +import { Policy, StackConfig } from '../../src/lib/types'; +import MockAdapter from 'axios-mock-adapter'; + +describe('Contentstack Debug Logging Integration', () => { + let mockLogHandler: jest.Mock; + + beforeEach(() => { + mockLogHandler = jest.fn(); + }); + + it('should execute debug logging for request interceptor', async () => { + const config: StackConfig = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + debug: true, + logHandler: mockLogHandler, + }; + + const stack = Contentstack.stack(config); + const client = stack.getClient(); + const mockClient = new MockAdapter(client); + + mockClient.onGet('/content_types/test').reply(200, { + content_types: [] + }); + + // Make an actual request to trigger interceptors + try { + await client.get('/content_types/test'); + } catch (e) { + // Ignore errors + } + + // Verify request logging was called + const requestLogs = mockLogHandler.mock.calls.filter((call: any) => + call[1]?.type === 'request' + ); + expect(requestLogs.length).toBeGreaterThan(0); + + mockClient.restore(); + }); + + it('should execute debug logging for response interceptor with 2xx status', async () => { + const config: StackConfig = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + debug: true, + logHandler: mockLogHandler, + }; + + const stack = Contentstack.stack(config); + const client = stack.getClient(); + const mockClient = new MockAdapter(client); + + mockClient.onGet('/content_types/test').reply(200, { + content_types: [] + }); + + await client.get('/content_types/test'); + + // Verify response logging was called with info level + const responseLogs = mockLogHandler.mock.calls.filter((call: any) => + call[1]?.type === 'response' && call[0] === 'info' + ); + expect(responseLogs.length).toBeGreaterThan(0); + + mockClient.restore(); + }); + + it('should execute debug logging for response interceptor with 3xx status', async () => { + const config: StackConfig = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + debug: true, + logHandler: mockLogHandler, + }; + + const stack = Contentstack.stack(config); + const client = stack.getClient(); + const mockClient = new MockAdapter(client); + + // 3xx responses are treated as errors by axios-mock-adapter + mockClient.onGet('/content_types/test').reply(304, {}); + + try { + await client.get('/content_types/test'); + } catch (e) { + // Expected - 3xx responses trigger error handler in mock adapter + } + + // Verify error response logging was called - 3xx goes through error interceptor + const errorLogs = mockLogHandler.mock.calls.filter((call: any) => + call[1]?.type === 'response_error' && call[1]?.status === 304 + ); + expect(errorLogs.length).toBeGreaterThan(0); + + mockClient.restore(); + }); + + it('should execute debug logging for error response interceptor with 4xx status', async () => { + const config: StackConfig = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + debug: true, + logHandler: mockLogHandler, + }; + + const stack = Contentstack.stack(config); + const client = stack.getClient(); + const mockClient = new MockAdapter(client); + + mockClient.onGet('/content_types/test').reply(404, { + error: 'Not found' + }); + + try { + await client.get('/content_types/test'); + } catch (e) { + // Expected error + } + + // Verify error logging was called + const errorLogs = mockLogHandler.mock.calls.filter((call: any) => + call[1]?.type === 'response_error' && call[0] === 'error' + ); + expect(errorLogs.length).toBeGreaterThan(0); + + mockClient.restore(); + }); + + it('should execute debug logging for error response without status', async () => { + const config: StackConfig = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + debug: true, + logHandler: mockLogHandler, + }; + + const stack = Contentstack.stack(config); + const client = stack.getClient(); + const mockClient = new MockAdapter(client); + + mockClient.onGet('/content_types/test').networkError(); + + try { + await client.get('/content_types/test'); + } catch (e) { + // Expected network error + } + + // Verify error logging was called with debug level for no status + const errorLogs = mockLogHandler.mock.calls.filter((call: any) => + call[1]?.type === 'response_error' && call[0] === 'debug' + ); + expect(errorLogs.length).toBeGreaterThan(0); + + mockClient.restore(); + }); + + it('should set cache adapter when cacheOptions is provided', () => { + const config: StackConfig = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + cacheOptions: { + policy: Policy.CACHE_THEN_NETWORK, + maxAge: 3600, + }, + }; + + const stack = Contentstack.stack(config); + const client = stack.getClient(); + + // Verify the custom adapter was set + const customAdapter = client.defaults.adapter; + expect(customAdapter).toBeDefined(); + expect(typeof customAdapter).toBe('function'); + }); + + it('should set cache adapter with NETWORK_ELSE_CACHE policy', () => { + const config: StackConfig = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + cacheOptions: { + policy: Policy.NETWORK_ELSE_CACHE, + maxAge: 3600, + }, + }; + + const stack = Contentstack.stack(config); + const client = stack.getClient(); + + const customAdapter = client.defaults.adapter; + expect(customAdapter).toBeDefined(); + expect(typeof customAdapter).toBe('function'); + }); + + it('should set cache adapter with CACHE_ELSE_NETWORK policy', () => { + const config: StackConfig = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + cacheOptions: { + policy: Policy.CACHE_ELSE_NETWORK, + maxAge: 3600, + }, + }; + + const stack = Contentstack.stack(config); + const client = stack.getClient(); + + const customAdapter = client.defaults.adapter; + expect(customAdapter).toBeDefined(); + expect(typeof customAdapter).toBe('function'); + }); +}); + diff --git a/test/unit/contentstack.spec.ts b/test/unit/contentstack.spec.ts index b65dc46..7a8fe02 100644 --- a/test/unit/contentstack.spec.ts +++ b/test/unit/contentstack.spec.ts @@ -3,6 +3,7 @@ import * as core from "@contentstack/core"; import * as Contentstack from "../../src/lib/contentstack"; import { Stack } from "../../src/lib/stack"; import { Policy, Region, StackConfig } from "../../src/lib/types"; +import { StorageType } from "../../src/persistance/types/storage-type"; import { CUSTOM_HOST, DUMMY_URL, @@ -401,4 +402,289 @@ describe("Contentstack", () => { getHostforRegionSpy.mockRestore(); }); }); + + describe('locale configuration', () => { + it('should set locale in params when locale is provided in config', () => { + const config = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + locale: "fr-fr", + }; + + const stackInstance = createStackInstance(config); + + expect(stackInstance).toBeInstanceOf(Stack); + expect(stackInstance.config.locale).toEqual("fr-fr"); + }); + }); + + describe('live preview configuration in browser environment', () => { + const originalDocument = global.document; + const originalWindow = global.window; + + beforeEach(() => { + // Mock browser environment + (utils.isBrowser as jest.Mock) = jest.fn(); + delete (global as any).document; + delete (global as any).window; + }); + + afterEach(() => { + global.document = originalDocument; + global.window = originalWindow; + jest.restoreAllMocks(); + }); + + it('should extract live_preview params from URL in browser environment', () => { + const isBrowserSpy = jest.spyOn(utils, 'isBrowser').mockReturnValue(true); + + // Mock document.location + const mockSearchParams = new Map([ + ['live_preview', 'test_hash'], + ['release_id', 'release123'], + ['preview_timestamp', '123456789'] + ]); + + (global as any).document = { + location: { + toString: () => 'http://localhost?live_preview=test_hash&release_id=release123&preview_timestamp=123456789' + } + }; + + const config = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + live_preview: { + enable: true, + live_preview: 'default_hash' + }, + }; + + const stackInstance = createStackInstance(config); + + expect(isBrowserSpy).toHaveBeenCalled(); + expect(stackInstance).toBeInstanceOf(Stack); + + isBrowserSpy.mockRestore(); + }); + + it('should use fallback value when live_preview param is empty (line 74 || branch)', () => { + const isBrowserSpy = jest.spyOn(utils, 'isBrowser').mockReturnValue(true); + + // Mock document.location with empty live_preview param + (global as any).document = { + location: { + toString: () => 'http://localhost?live_preview=' + } + }; + + const config = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + live_preview: { + enable: true, + live_preview: 'fallback_hash' + }, + }; + + const stackInstance = createStackInstance(config); + + // Should use the fallback value when params.get returns empty string + expect(stackInstance.config.live_preview?.live_preview).toBe('fallback_hash'); + + isBrowserSpy.mockRestore(); + }); + + it('should not extract params when not in browser environment', () => { + const isBrowserSpy = jest.spyOn(utils, 'isBrowser').mockReturnValue(false); + + const config = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + live_preview: { + enable: true, + }, + }; + + const stackInstance = createStackInstance(config); + + expect(isBrowserSpy).toHaveBeenCalled(); + expect(stackInstance).toBeInstanceOf(Stack); + + isBrowserSpy.mockRestore(); + }); + }); + + describe('cache adapter configuration', () => { + it('should set cache adapter when cacheOptions with policy is provided', () => { + const mockAdapter = jest.fn(); + const mockClient = { + defaults: { + host: HOST_URL, + adapter: mockAdapter, + }, + interceptors: { + request: { + use: reqInterceptor, + }, + response: { + use: resInterceptor, + }, + }, + }; + + createHttpClientMock.mockReturnValue(mockClient as any); + + const config = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + cacheOptions: { + policy: Policy.CACHE_THEN_NETWORK, + storeType: 'localStorage' as StorageType + }, + }; + + const stackInstance = createStackInstance(config); + + expect(stackInstance).toBeInstanceOf(Stack); + expect(mockClient.defaults.adapter).toBeDefined(); + }); + }); + + describe('debug mode with logging interceptors', () => { + it('should add request and response logging interceptors when debug is enabled', () => { + const mockLogHandler = jest.fn(); + const config = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + debug: true, + logHandler: mockLogHandler, + }; + + const stackInstance = createStackInstance(config); + + expect(stackInstance).toBeInstanceOf(Stack); + expect(reqInterceptor).toHaveBeenCalled(); + expect(resInterceptor).toHaveBeenCalled(); + }); + }); + + describe('plugin interceptors execution', () => { + it('should execute plugin onRequest and onResponse methods', () => { + const mockOnRequest = jest.fn((req) => req); + const mockOnResponse = jest.fn((req, res, data) => res); + let requestInterceptor: any; + let responseInterceptor: any; + + const mockClient = { + defaults: { + host: HOST_URL, + }, + interceptors: { + request: { + use: jest.fn((interceptor) => { + requestInterceptor = interceptor; + }), + }, + response: { + use: jest.fn((successInterceptor) => { + responseInterceptor = successInterceptor; + }), + }, + }, + }; + + createHttpClientMock.mockReturnValue(mockClient as any); + + const mockPlugin = { + onRequest: mockOnRequest, + onResponse: mockOnResponse, + }; + + const config = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + plugins: [mockPlugin], + }; + + createStackInstance(config); + + // Test that interceptors were registered + expect(mockClient.interceptors.request.use).toHaveBeenCalled(); + expect(mockClient.interceptors.response.use).toHaveBeenCalled(); + + // Test request interceptor execution + const mockRequest = { url: '/test' }; + requestInterceptor(mockRequest); + expect(mockOnRequest).toHaveBeenCalledWith(mockRequest); + + // Test response interceptor execution + const mockResponse = { + request: {}, + data: {}, + }; + responseInterceptor(mockResponse); + expect(mockOnResponse).toHaveBeenCalledWith(mockResponse.request, mockResponse, mockResponse.data); + }); + + it('should handle multiple plugins in order', () => { + const executionOrder: string[] = []; + let requestInterceptor: any; + + const mockClient = { + defaults: { + host: HOST_URL, + }, + interceptors: { + request: { + use: jest.fn((interceptor) => { + requestInterceptor = interceptor; + }), + }, + response: { + use: jest.fn(), + }, + }, + }; + + createHttpClientMock.mockReturnValue(mockClient as any); + + const mockPlugin1 = { + onRequest: jest.fn((req) => { + executionOrder.push('plugin1'); + return req; + }), + onResponse: jest.fn((req, res, data) => res), + }; + + const mockPlugin2 = { + onRequest: jest.fn((req) => { + executionOrder.push('plugin2'); + return req; + }), + onResponse: jest.fn((req, res, data) => res), + }; + + const config = { + apiKey: "apiKey", + deliveryToken: "delivery", + environment: "env", + plugins: [mockPlugin1, mockPlugin2], + }; + + createStackInstance(config); + + const mockRequest = { url: '/test' }; + requestInterceptor(mockRequest); + + expect(executionOrder).toEqual(['plugin1', 'plugin2']); + }); + }); }); diff --git a/test/unit/contenttype.spec.ts b/test/unit/contenttype.spec.ts index 1b62c34..20b821d 100644 --- a/test/unit/contenttype.spec.ts +++ b/test/unit/contenttype.spec.ts @@ -41,4 +41,13 @@ describe('ContentType class', () => { const response = await contentType.fetch(); expect(response).toEqual(contentTypeResponseMock.content_type); }); + + it('should return response directly when content_type property is not present', async () => { + const responseWithoutContentType = { data: 'test', uid: 'test-uid' }; + mockClient.onGet('/content_types/contentTypeUid').reply(200, responseWithoutContentType); + + const result = await contentType.fetch(); + + expect(result).toEqual(responseWithoutContentType); + }); }); diff --git a/test/unit/entries.spec.ts b/test/unit/entries.spec.ts index 020b3ae..aa6f233 100644 --- a/test/unit/entries.spec.ts +++ b/test/unit/entries.spec.ts @@ -38,6 +38,18 @@ describe('Entries class', () => { expect(entry._queryParams['include[]']).toContain(referenceFieldUid); }); + it('should handle multiple reference field UIDs', () => { + entry.includeReference('ref1', 'ref2', ['ref3', 'ref4']); + expect(entry._queryParams['include[]']).toEqual(['ref1', 'ref2', 'ref3', 'ref4']); + }); + + it('should log error when includeReference called with no arguments', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + entry.includeReference(); + expect(consoleErrorSpy).toHaveBeenCalledWith('Argument should be a String or an Array.'); + consoleErrorSpy.mockRestore(); + }); + it('should add "include_fallback" in _queryParams when includeFallback method is called', () => { const returnedValue = entry.includeFallback(); expect(returnedValue).toBeInstanceOf(Entries); @@ -79,6 +91,13 @@ describe('Entries class', () => { expect(returnedValue).toBeInstanceOf(Query); }); + it('should return Query instance with queryObj when query method is called with object', () => { + const queryObj = { title: 'Test' }; + const returnedValue = entry.query(queryObj); + expect(returnedValue).toBeInstanceOf(Query); + expect(returnedValue._parameters).toEqual(queryObj); + }); + it('should add a fieldUid to the _queryParams object', () => { entry.only('fieldUid'); expect(entry._queryParams).toEqual({ 'only[BASE][]': 'fieldUid' }); @@ -109,6 +128,20 @@ describe('Entries class', () => { expect(entry._queryParams).toEqual({ 'except[BASE][]': 'fieldUid2' }); }); + it('should handle except with array of fieldUids', () => { + entry.except(['field1', 'field2', 'field3']); + expect(entry._queryParams['except[BASE][0]']).toBe('field1'); + expect(entry._queryParams['except[BASE][1]']).toBe('field2'); + expect(entry._queryParams['except[BASE][2]']).toBe('field3'); + }); + + it('should handle only with array of fieldUids', () => { + entry.only(['field1', 'field2', 'field3']); + expect(entry._queryParams['only[BASE][0]']).toBe('field1'); + expect(entry._queryParams['only[BASE][1]']).toBe('field2'); + expect(entry._queryParams['only[BASE][2]']).toBe('field3'); + }); + it('should provide proper response when find method is called', async () => { mockClient.onGet(`/content_types/contentTypeUid/entries`).reply(200, entryFindMock); const returnedValue = await entry.find(); @@ -176,6 +209,10 @@ class TestVariants extends Entries { this.variants(['variant1', 'variant2']); return this._variants || ""; } + + getVariants(): string { + return this._variants || ""; + } } describe('Variants test', () => { @@ -191,4 +228,76 @@ describe('Variants test', () => { expect(testVariantObj.setAndGetVariantsHeaders()).toBe('variant1,variant2'); }); + + it('should set variants as string', () => { + const testVariantObj = new TestVariants(client); + testVariantObj.variants('variant1'); + expect(testVariantObj.getVariants()).toBe('variant1'); + }); + + it('should set variants as comma-separated string from array', () => { + const testVariantObj = new TestVariants(client); + testVariantObj.variants(['variant1', 'variant2', 'variant3']); + expect(testVariantObj.getVariants()).toBe('variant1,variant2,variant3'); + }); + + it('should not set variants when empty string is provided', () => { + const testVariantObj = new TestVariants(client); + testVariantObj.variants(''); + expect(testVariantObj.getVariants()).toBe(''); + }); + + it('should not set variants when empty array is provided', () => { + const testVariantObj = new TestVariants(client); + testVariantObj.variants([]); + expect(testVariantObj.getVariants()).toBe(''); + }); +}); + +describe('Find with encode and variants', () => { + let client: AxiosInstance; + let mockClient: MockAdapter; + let entry: Entries; + + beforeAll(() => { + client = httpClient(MOCK_CLIENT_OPTIONS); + mockClient = new MockAdapter(client as any); + }); + + beforeEach(() => { + entry = new Entries(client, 'contentTypeUid'); + mockClient.reset(); + }); + + it('should call find with encode parameter true', async () => { + mockClient.onGet('/content_types/contentTypeUid/entries').reply(200, entryFindMock); + + entry.query().where('title', QueryOperation.EQUALS, 'Test'); + const result = await entry.find(true); + + expect(result).toEqual(entryFindMock); + }); + + it('should call find with variants header when variants are set', async () => { + mockClient.onGet('/content_types/contentTypeUid/entries').reply((config) => { + expect(config.headers?.['x-cs-variant-uid']).toBe('variant1,variant2'); + return [200, entryFindMock]; + }); + + entry.variants(['variant1', 'variant2']); + await entry.find(); + }); + + it('should handle find with both encode and variants', async () => { + mockClient.onGet('/content_types/contentTypeUid/entries').reply((config) => { + expect(config.headers?.['x-cs-variant-uid']).toBe('test-variant'); + return [200, entryFindMock]; + }); + + entry.variants('test-variant'); + entry.query().where('status', QueryOperation.EQUALS, 'published'); + const result = await entry.find(true); + + expect(result).toEqual(entryFindMock); + }); }) \ No newline at end of file diff --git a/test/unit/entry.spec.ts b/test/unit/entry.spec.ts index 58b9453..47bc5d4 100644 --- a/test/unit/entry.spec.ts +++ b/test/unit/entry.spec.ts @@ -44,6 +44,18 @@ describe('Entry class', () => { expect(entry._queryParams['include[]']).toContain(referenceFieldUid); }); + it('should handle multiple reference field UIDs', () => { + entry.includeReference('ref1', 'ref2', ['ref3', 'ref4']); + expect(entry._queryParams['include[]']).toEqual(['ref1', 'ref2', 'ref3', 'ref4']); + }); + + it('should log error when includeReference called with no arguments', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + entry.includeReference(); + expect(consoleErrorSpy).toHaveBeenCalledWith('Argument should be a String or an Array.'); + consoleErrorSpy.mockRestore(); + }); + it('should add "include_metadata" in _queryParams when includeMetadata method is called', () => { const returnedValue = entry.includeMetadata(); expect(returnedValue).toBeInstanceOf(Entry); @@ -77,6 +89,13 @@ describe('Entry class', () => { expect(entry._queryParams).toEqual({ 'only[BASE][]': 'fieldUid2' }); }); + it('should handle only with array of fieldUids', () => { + entry.only(['field1', 'field2', 'field3']); + expect(entry._queryParams['only[BASE][0]']).toBe('field1'); + expect(entry._queryParams['only[BASE][1]']).toBe('field2'); + expect(entry._queryParams['only[BASE][2]']).toBe('field3'); + }); + it('should add a fieldUid to the _queryParams object', () => { entry.except('fieldUid'); expect(entry._queryParams).toEqual({ 'except[BASE][]': 'fieldUid' }); @@ -92,6 +111,22 @@ describe('Entry class', () => { expect(entry._queryParams).toEqual({ 'except[BASE][]': 'fieldUid2' }); }); + it('should handle except with array of fieldUids', () => { + entry.except(['field1', 'field2', 'field3']); + expect(entry._queryParams['except[BASE][0]']).toBe('field1'); + expect(entry._queryParams['except[BASE][1]']).toBe('field2'); + expect(entry._queryParams['except[BASE][2]']).toBe('field3'); + }); + + it('should add params to _queryParams using addParams', () => { + const params = { key1: 'value1', key2: 123, key3: ['value3'] }; + const returnedValue = entry.addParams(params); + expect(returnedValue).toBeInstanceOf(Entry); + expect(entry._queryParams.key1).toBe('value1'); + expect(entry._queryParams.key2).toBe(123); + expect(entry._queryParams.key3).toEqual(['value3']); + }); + it('should get the API response when fetch method is called', async () => { mockClient.onGet(`/content_types/contentTypeUid/entries/entryUid`).reply(200, entryFetchMock); const returnedValue = await entry.fetch(); @@ -110,6 +145,10 @@ class TestVariants extends Entry { this.variants(['variant1', 'variant2']); // setting the variants headers so it doesnt give empty string return this._variants || ""; } + + getVariants(): string { + return this._variants || ""; + } } describe('Variants test', () => { @@ -125,4 +164,76 @@ describe('Variants test', () => { expect(testVariantObj.setAndGetVariantsHeaders()).toBe('variant1,variant2'); }); + + it('should set variants as string', () => { + const testVariantObj = new TestVariants(client); + testVariantObj.variants('variant1'); + expect(testVariantObj.getVariants()).toBe('variant1'); + }); + + it('should set variants as comma-separated string from array', () => { + const testVariantObj = new TestVariants(client); + testVariantObj.variants(['variant1', 'variant2', 'variant3']); + expect(testVariantObj.getVariants()).toBe('variant1,variant2,variant3'); + }); + + it('should not set variants when empty string is provided', () => { + const testVariantObj = new TestVariants(client); + testVariantObj.variants(''); + expect(testVariantObj.getVariants()).toBe(''); + }); + + it('should not set variants when empty array is provided', () => { + const testVariantObj = new TestVariants(client); + testVariantObj.variants([]); + expect(testVariantObj.getVariants()).toBe(''); + }); +}); + +describe('Fetch with variants', () => { + let client: AxiosInstance; + let mockClient: MockAdapter; + let entry: Entry; + + beforeAll(() => { + client = httpClient(MOCK_CLIENT_OPTIONS); + mockClient = new MockAdapter(client as any); + }); + + beforeEach(() => { + entry = new Entry(client, 'contentTypeUid', 'entryUid'); + mockClient.reset(); + }); + + it('should call fetch with variants header when variants are set', async () => { + mockClient.onGet('/content_types/contentTypeUid/entries/entryUid').reply((config) => { + expect(config.headers?.['x-cs-variant-uid']).toBe('variant1,variant2'); + return [200, entryFetchMock]; + }); + + entry.variants(['variant1', 'variant2']); + const result = await entry.fetch(); + + expect(result).toEqual(entryFetchMock.entry); + }); + + it('should call fetch without variant header when variants are not set', async () => { + mockClient.onGet('/content_types/contentTypeUid/entries/entryUid').reply((config) => { + expect(config.headers?.['x-cs-variant-uid']).toBeUndefined(); + return [200, entryFetchMock]; + }); + + const result = await entry.fetch(); + + expect(result).toEqual(entryFetchMock.entry); + }); + + it('should return response directly when entry property is not present', async () => { + const responseWithoutEntry = { data: 'test', uid: 'test-uid' }; + mockClient.onGet('/content_types/contentTypeUid/entries/entryUid').reply(200, responseWithoutEntry); + + const result = await entry.fetch(); + + expect(result).toEqual(responseWithoutEntry); + }); }) diff --git a/test/unit/pagination.spec.ts b/test/unit/pagination.spec.ts index 92a0d64..ff999a5 100644 --- a/test/unit/pagination.spec.ts +++ b/test/unit/pagination.spec.ts @@ -25,4 +25,27 @@ describe('Pagination class', () => { pageObject.previous(); expect(pageObject._queryParams).toEqual({ skip: 0, limit: 10 }); }); + + it('should initialize pagination when next is called without prior paginate', () => { + const pageObject = new Pagination(); + pageObject.next(); + expect(pageObject._queryParams).toEqual({ skip: 10, limit: 10 }); + }); + + it('should initialize pagination when previous is called without prior paginate', () => { + const pageObject = new Pagination(); + pageObject.previous(); + expect(pageObject._queryParams).toEqual({ skip: 0, limit: 10 }); + }); + + it('should set skip to 0 when previous would result in negative skip', () => { + const pageObject = new Pagination().paginate({ skip: 5, limit: 10 }); + pageObject.previous(); + expect(pageObject._queryParams.skip).toEqual(0); + }); + + it('should use default values when paginate is called without arguments', () => { + const pageObject = new Pagination().paginate(); + expect(pageObject._queryParams).toEqual({ skip: 0, limit: 10 }); + }); }); diff --git a/test/unit/query.spec.ts b/test/unit/query.spec.ts index 301d124..d244a27 100644 --- a/test/unit/query.spec.ts +++ b/test/unit/query.spec.ts @@ -55,7 +55,7 @@ describe('Query class', () => { }); it('should add a where-in filter to the query parameters', () => { - const subQuery = getQueryObject(client, 'your-referenced-content-type-uid'); + const subQuery = getQueryObject(client, 'referenced-content-type-uid'); subQuery.where('your-field-uid', QueryOperation.EQUALS, 'your-field-value'); query.whereIn('your-reference-field-uid', subQuery); // eslint-disable-next-line prettier/prettier, @typescript-eslint/naming-convention @@ -63,7 +63,7 @@ describe('Query class', () => { }); it('should add a where-not-in filter to the query parameters', () => { - const subQuery = getQueryObject(client, 'your-referenced-content-type-uid'); + const subQuery = getQueryObject(client, 'referenced-content-type-uid'); subQuery.where('your-field-uid', QueryOperation.EQUALS, 'your-field-value'); query.whereNotIn('your-reference-field-uid', subQuery); // eslint-disable-next-line prettier/prettier, @typescript-eslint/naming-convention @@ -84,10 +84,15 @@ describe('Query class', () => { }); it('should result in error when regex method is called with invalid regex', async () => { - const regexQuery = getQueryObject(client, 'your-referenced-content-type-uid'); + const regexQuery = getQueryObject(client, 'referenced-content-type-uid'); expect(() => regexQuery.regex("fieldUid", "[a-z")).toThrow("Invalid regexPattern: Must be a valid regular expression"); }); + it('should throw error when regex method is called with invalid characters', async () => { + const regexQuery = getQueryObject(client, 'referenced-content-type-uid'); + expect(() => regexQuery.regex("fieldUid", "test