From 5ebe2a75b848b79f1db3de1d25f15a14d4c8528f Mon Sep 17 00:00:00 2001 From: swati354 <103816801+swati354@users.noreply.github.com> Date: Tue, 14 Oct 2025 11:49:44 +0530 Subject: [PATCH 1/7] Add unit tests (#99) --- .gitignore | 5 +---- jest.config.cjs | 6 ------ package-lock.json | 5 ++++- package.json | 12 ++++++++++-- tests/unit/services/tasks.test.ts | 0 tests/utils/mocks/core.ts | 0 tests/utils/setup.ts | 0 vitest.config.ts | 24 ++++++++++++++++++++++++ 8 files changed, 39 insertions(+), 13 deletions(-) delete mode 100644 jest.config.cjs create mode 100644 tests/unit/services/tasks.test.ts create mode 100644 tests/utils/mocks/core.ts create mode 100644 tests/utils/setup.ts create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index 4859cd19..a2bcdae6 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,4 @@ npm-debug.log* dist/ docs/api/ -site/ - -tests/ -jest.config.* \ No newline at end of file +site/ \ No newline at end of file diff --git a/jest.config.cjs b/jest.config.cjs deleted file mode 100644 index a50751ce..00000000 --- a/jest.config.cjs +++ /dev/null @@ -1,6 +0,0 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} */ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/tests/**/*.test.ts'], -}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e793c81d..76591d33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,8 @@ "@rollup/plugin-typescript": "^11.1.0", "@types/node": "^20.11.19", "@types/uuid": "^9.0.8", + "@vitest/coverage-v8": "^3.2.4", + "@vitest/ui": "^3.2.4", "builtin-modules": "^3.3.0", "dotenv": "^17.2.0", "rimraf": "^6.0.1", @@ -30,7 +32,8 @@ "rollup-plugin-dts": "^6.1.0", "typedoc": "^0.28.13", "typedoc-plugin-markdown": "^4.8.1", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^3.2.4" } }, "node_modules/@azure/abort-controller": { diff --git a/package.json b/package.json index a0fe53fd..9e498765 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,12 @@ "build": "rollup -c", "build:watch": "rollup -c -w", "clean": "rimraf dist && rimraf node_modules && rimraf package-lock.json", - "docs:api": "typedoc" + "docs:api": "typedoc", + "test": "vitest", + "test:unit": "vitest tests/unit", + "test:watch": "vitest --watch", + "test:ui": "vitest --ui", + "test:coverage": "vitest --coverage" }, "dependencies": { "@opentelemetry/sdk-logs": "^0.204.0", @@ -49,6 +54,8 @@ "@rollup/plugin-typescript": "^11.1.0", "@types/node": "^20.11.19", "@types/uuid": "^9.0.8", + "@vitest/coverage-v8": "^3.2.4", + "@vitest/ui": "^3.2.4", "builtin-modules": "^3.3.0", "dotenv": "^17.2.0", "rimraf": "^6.0.1", @@ -56,7 +63,8 @@ "rollup-plugin-dts": "^6.1.0", "typedoc": "^0.28.13", "typedoc-plugin-markdown": "^4.8.1", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^3.2.4" }, "repository": { "type": "git", diff --git a/tests/unit/services/tasks.test.ts b/tests/unit/services/tasks.test.ts new file mode 100644 index 00000000..e69de29b diff --git a/tests/utils/mocks/core.ts b/tests/utils/mocks/core.ts new file mode 100644 index 00000000..e69de29b diff --git a/tests/utils/setup.ts b/tests/utils/setup.ts new file mode 100644 index 00000000..e69de29b diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..4c4bc066 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['tests/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: [ + 'node_modules/', + 'tests/', + 'dist/', + 'samples/**', + 'packages/cli/**', + 'docs/**', + '**/*.d.ts', + '**/*.config.*', + '**/index.ts', + ], + }, + }, +}); From c76bb031ffc98544ee7721af635718d09cd0b4f0 Mon Sep 17 00:00:00 2001 From: swati354 <103816801+swati354@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:20:41 +0530 Subject: [PATCH 2/7] Add maestro process and process instance test cases (#101) Co-authored-by: Raina451 --- src/models/orchestrator/buckets.models.ts | 2 +- tests/unit/models/maestro/index.ts | 5 + .../models/maestro/process-instances.test.ts | 394 ++++++++++++ tests/unit/services/maestro/index.ts | 3 + .../maestro/process-instances.test.ts | 568 ++++++++++++++++++ tests/unit/services/maestro/processes.test.ts | 155 +++++ tests/utils/constants/common.ts | 24 + tests/utils/constants/index.ts | 6 + tests/utils/constants/maestro.ts | 52 ++ tests/utils/mocks/core.ts | 83 +++ tests/utils/mocks/index.ts | 13 + tests/utils/mocks/maestro.ts | 170 ++++++ tests/utils/setup.ts | 91 +++ 13 files changed, 1565 insertions(+), 1 deletion(-) create mode 100644 tests/unit/models/maestro/index.ts create mode 100644 tests/unit/models/maestro/process-instances.test.ts create mode 100644 tests/unit/services/maestro/index.ts create mode 100644 tests/unit/services/maestro/process-instances.test.ts create mode 100644 tests/unit/services/maestro/processes.test.ts create mode 100644 tests/utils/constants/common.ts create mode 100644 tests/utils/constants/index.ts create mode 100644 tests/utils/constants/maestro.ts create mode 100644 tests/utils/mocks/index.ts create mode 100644 tests/utils/mocks/maestro.ts diff --git a/src/models/orchestrator/buckets.models.ts b/src/models/orchestrator/buckets.models.ts index 66e10e12..aebc71e8 100644 --- a/src/models/orchestrator/buckets.models.ts +++ b/src/models/orchestrator/buckets.models.ts @@ -1,4 +1,4 @@ -import { BucketGetAllOptions, BucketGetByIdOptions, BucketGetResponse, BucketGetFileMetaDataWithPaginationOptions, BucketGetFileMetaDataResponse, BucketGetReadUriOptions, BucketGetUriResponse, BucketUploadFileOptions, BucketUploadResponse, BlobItem } from './buckets.types'; +import { BucketGetAllOptions, BucketGetByIdOptions, BucketGetResponse, BucketGetFileMetaDataWithPaginationOptions, BucketGetReadUriOptions, BucketGetUriResponse, BucketUploadFileOptions, BucketUploadResponse, BlobItem } from './buckets.types'; import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '../../utils/pagination'; /** diff --git a/tests/unit/models/maestro/index.ts b/tests/unit/models/maestro/index.ts new file mode 100644 index 00000000..f6395f98 --- /dev/null +++ b/tests/unit/models/maestro/index.ts @@ -0,0 +1,5 @@ +/** + * Maestro models test exports + */ + +export * from './process-instances.test'; \ No newline at end of file diff --git a/tests/unit/models/maestro/process-instances.test.ts b/tests/unit/models/maestro/process-instances.test.ts new file mode 100644 index 00000000..c15ca975 --- /dev/null +++ b/tests/unit/models/maestro/process-instances.test.ts @@ -0,0 +1,394 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + createProcessInstanceWithMethods, + ProcessInstancesServiceModel +} from '../../../../src/models/maestro/process-instances.models'; +import { + MAESTRO_TEST_CONSTANTS, + TEST_CONSTANTS, + createMockOperationResponse, + createMockProcessInstance, + createMockExecutionHistory, + createMockProcessVariables, + createMockBpmnWithVariables +} from '../../../utils/mocks'; +import type { + ProcessInstanceOperationOptions, + ProcessInstanceGetVariablesOptions, +} from '../../../../src/models/maestro/process-instances.types'; + +// ===== TEST SUITE ===== +describe('Process Instance Models', () => { + let mockService: ProcessInstancesServiceModel; + + beforeEach(() => { + // Create a mock service + mockService = { + getAll: vi.fn(), + getById: vi.fn(), + getExecutionHistory: vi.fn(), + getBpmn: vi.fn(), + cancel: vi.fn(), + pause: vi.fn(), + resume: vi.fn(), + getVariables: vi.fn() + } as any; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('bound methods on process instance', () => { + describe('processInstance.cancel()', () => { + it('should call processInstance.cancel with bound instanceId and folderKey', async () => { + const mockInstanceData = createMockProcessInstance(); + const instance = createProcessInstanceWithMethods(mockInstanceData, mockService); + + const mockResponse = createMockOperationResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + status: TEST_CONSTANTS.CANCELLED + }); + mockService.cancel = vi.fn().mockResolvedValue(mockResponse); + + + await instance.cancel(); + + + expect(mockService.cancel).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + undefined + ); + }); + + it('should call processInstance.cancel with bound parameters and options', async () => { + const mockInstanceData = createMockProcessInstance(); + const instance = createProcessInstanceWithMethods(mockInstanceData, mockService); + + const mockResponse = createMockOperationResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + status: TEST_CONSTANTS.CANCELLED + }); + const options: ProcessInstanceOperationOptions = { comment: 'Test cancellation' }; + mockService.cancel = vi.fn().mockResolvedValue(mockResponse); + + + await instance.cancel(options); + + + expect(mockService.cancel).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + options + ); + }); + + it('should throw error if instanceId is undefined', async () => { + const mockInstanceData = createMockProcessInstance(); + const invalidInstanceData = { ...mockInstanceData, instanceId: undefined as any }; + const invalidInstance = createProcessInstanceWithMethods(invalidInstanceData, mockService); + + + await expect(invalidInstance.cancel()).rejects.toThrow('Process instance ID is undefined'); + }); + + it('should throw error if folderKey is undefined', async () => { + const mockInstanceData = createMockProcessInstance(); + const invalidInstanceData = { ...mockInstanceData, folderKey: undefined as any }; + const invalidInstance = createProcessInstanceWithMethods(invalidInstanceData, mockService); + + + await expect(invalidInstance.cancel()).rejects.toThrow('Process instance folder key is undefined'); + }); + }); + + describe('processInstance.pause()', () => { + it('should call processInstance.pause with bound instanceId and folderKey', async () => { + const mockInstanceData = createMockProcessInstance(); + const instance = createProcessInstanceWithMethods(mockInstanceData, mockService); + + const mockResponse = createMockOperationResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + status: TEST_CONSTANTS.CANCELLED + }); + mockService.pause = vi.fn().mockResolvedValue(mockResponse); + + + await instance.pause(); + + + expect(mockService.pause).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + undefined + ); + }); + + it('should call processInstance.pause with bound parameters and options', async () => { + const mockInstanceData = createMockProcessInstance(); + const instance = createProcessInstanceWithMethods(mockInstanceData, mockService); + + const mockResponse = createMockOperationResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + status: TEST_CONSTANTS.CANCELLED + }); + const options: ProcessInstanceOperationOptions = { comment: 'Test pause' }; + mockService.pause = vi.fn().mockResolvedValue(mockResponse); + + + await instance.pause(options); + + + expect(mockService.pause).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + options + ); + }); + + it('should throw error if instanceId is undefined', async () => { + const mockInstanceData = createMockProcessInstance(); + const invalidInstanceData = { ...mockInstanceData, instanceId: undefined as any }; + const invalidInstance = createProcessInstanceWithMethods(invalidInstanceData, mockService); + + + await expect(invalidInstance.pause()).rejects.toThrow('Process instance ID is undefined'); + }); + + it('should throw error if folderKey is undefined', async () => { + const mockInstanceData = createMockProcessInstance(); + const invalidInstanceData = { ...mockInstanceData, folderKey: undefined as any }; + const invalidInstance = createProcessInstanceWithMethods(invalidInstanceData, mockService); + + + await expect(invalidInstance.pause()).rejects.toThrow('Process instance folder key is undefined'); + }); + }); + + describe('processInstance.resume()', () => { + it('should call processInstance.resume with bound instanceId and folderKey', async () => { + const mockInstanceData = createMockProcessInstance(); + const instance = createProcessInstanceWithMethods(mockInstanceData, mockService); + + const mockResponse = createMockOperationResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + status: TEST_CONSTANTS.CANCELLED + }); + mockService.resume = vi.fn().mockResolvedValue(mockResponse); + + + await instance.resume(); + + + expect(mockService.resume).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + undefined + ); + }); + + it('should call service.resume with bound parameters and options', async () => { + const mockInstanceData = createMockProcessInstance(); + const instance = createProcessInstanceWithMethods(mockInstanceData, mockService); + + const mockResponse = createMockOperationResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + status: TEST_CONSTANTS.CANCELLED + }); + const options: ProcessInstanceOperationOptions = { comment: 'Test resume' }; + mockService.resume = vi.fn().mockResolvedValue(mockResponse); + + + await instance.resume(options); + + + expect(mockService.resume).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + options + ); + }); + + it('should throw error if instanceId is undefined', async () => { + const mockInstanceData = createMockProcessInstance(); + const invalidInstanceData = { ...mockInstanceData, instanceId: undefined as any }; + const invalidInstance = createProcessInstanceWithMethods(invalidInstanceData, mockService); + + + await expect(invalidInstance.resume()).rejects.toThrow('Process instance ID is undefined'); + }); + + it('should throw error if folderKey is undefined', async () => { + const mockInstanceData = createMockProcessInstance(); + const invalidInstanceData = { ...mockInstanceData, folderKey: undefined as any }; + const invalidInstance = createProcessInstanceWithMethods(invalidInstanceData, mockService); + + + await expect(invalidInstance.resume()).rejects.toThrow('Process instance folder key is undefined'); + }); + }); + + describe('processInstance.getExecutionHistory()', () => { + it('should call processInstance.getExecutionHistory with bound instanceId', async () => { + const mockInstanceData = createMockProcessInstance(); + const instance = createProcessInstanceWithMethods(mockInstanceData, mockService); + + const mockHistory = [createMockExecutionHistory()]; + mockService.getExecutionHistory = vi.fn().mockResolvedValue(mockHistory); + + + const result = await instance.getExecutionHistory(); + + + expect(mockService.getExecutionHistory).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.INSTANCE_ID + ); + expect(result).toEqual(mockHistory); + }); + + it('should throw error if instanceId is undefined', async () => { + const mockInstanceData = createMockProcessInstance(); + const invalidInstanceData = { ...mockInstanceData, instanceId: undefined as any }; + const invalidInstance = createProcessInstanceWithMethods(invalidInstanceData, mockService); + + + await expect(invalidInstance.getExecutionHistory()).rejects.toThrow('Process instance ID is undefined'); + }); + }); + + describe('processInstance.getBpmn()', () => { + it('should call processInstance.getBpmn with bound instanceId and folderKey', async () => { + const mockInstanceData = createMockProcessInstance(); + const instance = createProcessInstanceWithMethods(mockInstanceData, mockService); + + const mockBpmn = createMockBpmnWithVariables({ processId: 'SimpleProcess' }); + mockService.getBpmn = vi.fn().mockResolvedValue(mockBpmn); + + + const result = await instance.getBpmn(); + + + expect(mockService.getBpmn).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY + ); + expect(result).toBe(mockBpmn); + }); + + it('should throw error if instanceId is undefined', async () => { + const mockInstanceData = createMockProcessInstance(); + const invalidInstanceData = { ...mockInstanceData, instanceId: undefined as any }; + const invalidInstance = createProcessInstanceWithMethods(invalidInstanceData, mockService); + + + await expect(invalidInstance.getBpmn()).rejects.toThrow('Process instance ID is undefined'); + }); + + it('should throw error if folderKey is undefined', async () => { + const mockInstanceData = createMockProcessInstance(); + const invalidInstanceData = { ...mockInstanceData, folderKey: undefined as any }; + const invalidInstance = createProcessInstanceWithMethods(invalidInstanceData, mockService); + + + await expect(invalidInstance.getBpmn()).rejects.toThrow('Process instance folder key is undefined'); + }); + }); + + describe('processInstance.getVariables()', () => { + it('should call processInstance.getVariables with bound instanceId and folderKey', async () => { + const mockInstanceData = createMockProcessInstance(); + const instance = createProcessInstanceWithMethods(mockInstanceData, mockService); + + const mockVariables = createMockProcessVariables(); + mockService.getVariables = vi.fn().mockResolvedValue(mockVariables); + + + const result = await instance.getVariables(); + + + expect(mockService.getVariables).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + undefined + ); + expect(result).toEqual(mockVariables); + }); + + it('should call service.getVariables with bound parameters and options', async () => { + const mockInstanceData = createMockProcessInstance(); + const instance = createProcessInstanceWithMethods(mockInstanceData, mockService); + + const mockVariables = { + elements: [], + globalVariables: [], + instanceId: MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + parentElementId: MAESTRO_TEST_CONSTANTS.PARENT_ELEMENT_ID + }; + const options: ProcessInstanceGetVariablesOptions = { parentElementId: MAESTRO_TEST_CONSTANTS.PARENT_ELEMENT_ID }; + mockService.getVariables = vi.fn().mockResolvedValue(mockVariables); + + + const result = await instance.getVariables(options); + + + expect(mockService.getVariables).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + options + ); + expect(result).toEqual(mockVariables); + }); + + it('should throw error if instanceId is undefined', async () => { + const mockInstanceData = createMockProcessInstance(); + const invalidInstanceData = { ...mockInstanceData, instanceId: undefined as any }; + const invalidInstance = createProcessInstanceWithMethods(invalidInstanceData, mockService); + + + await expect(invalidInstance.getVariables()).rejects.toThrow('Process instance ID is undefined'); + }); + + it('should throw error if folderKey is undefined', async () => { + const mockInstanceData = createMockProcessInstance(); + const invalidInstanceData = { ...mockInstanceData, folderKey: undefined as any }; + const invalidInstance = createProcessInstanceWithMethods(invalidInstanceData, mockService); + + + await expect(invalidInstance.getVariables()).rejects.toThrow('Process instance folder key is undefined'); + }); + }); + }); + + describe('createProcessInstanceWithMethods', () => { + it('should create instance with all bound methods', () => { + const mockInstanceData = createMockProcessInstance(); + + const instance = createProcessInstanceWithMethods(mockInstanceData, mockService); + + + expect(instance).toHaveProperty('instanceId', MAESTRO_TEST_CONSTANTS.INSTANCE_ID); + expect(instance).toHaveProperty('folderKey', MAESTRO_TEST_CONSTANTS.FOLDER_KEY); + expect(instance).toHaveProperty('cancel'); + expect(instance).toHaveProperty('pause'); + expect(instance).toHaveProperty('resume'); + expect(instance).toHaveProperty('getExecutionHistory'); + expect(instance).toHaveProperty('getBpmn'); + expect(instance).toHaveProperty('getVariables'); + }); + + it('should preserve all original instance data', () => { + const mockInstanceData = createMockProcessInstance(); + + const instance = createProcessInstanceWithMethods(mockInstanceData, mockService); + + + expect(instance.instanceId).toBe(mockInstanceData.instanceId); + expect(instance.packageId).toBe(mockInstanceData.packageId); + expect(instance.processKey).toBe(mockInstanceData.processKey); + expect(instance.folderKey).toBe(mockInstanceData.folderKey); + expect(instance.latestRunStatus).toBe(mockInstanceData.latestRunStatus); + }); + + }); +}); \ No newline at end of file diff --git a/tests/unit/services/maestro/index.ts b/tests/unit/services/maestro/index.ts new file mode 100644 index 00000000..3e3b81ee --- /dev/null +++ b/tests/unit/services/maestro/index.ts @@ -0,0 +1,3 @@ +// Export all maestro service tests +export * from './processes.test'; +export * from './process-instances.test'; \ No newline at end of file diff --git a/tests/unit/services/maestro/process-instances.test.ts b/tests/unit/services/maestro/process-instances.test.ts new file mode 100644 index 00000000..73bca3c3 --- /dev/null +++ b/tests/unit/services/maestro/process-instances.test.ts @@ -0,0 +1,568 @@ +// ===== IMPORTS ===== +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { ProcessInstancesService } from '../../../../src/services/maestro/process-instances'; +import { MAESTRO_ENDPOINTS } from '../../../../src/utils/constants/endpoints'; +import { ApiClient } from '../../../../src/core/http/api-client'; +import { FOLDER_KEY, CONTENT_TYPES } from '../../../../src/utils/constants/headers'; +import { PaginationHelpers } from '../../../../src/utils/pagination/helpers'; +import { + MAESTRO_TEST_CONSTANTS, + TEST_CONSTANTS, + createMockProcessInstance, + createMockBpmnWithVariables, + createMockExecutionHistory, + createMockProcessVariables, + createMockMaestroApiOperationResponse +} from '../../../utils/mocks'; +import { createServiceTestDependencies, createMockApiClient } from '../../../utils/setup'; +import type { + ProcessInstanceGetAllWithPaginationOptions, + ProcessInstanceOperationOptions, + ProcessInstanceGetVariablesOptions, + RawProcessInstanceGetResponse, + ProcessInstanceExecutionHistoryResponse +} from '../../../../src/models/maestro'; + +// ===== MOCKING ===== +// Mock the dependencies +vi.mock('../../../../src/core/http/api-client'); + +// Use vi.hoisted to ensure mockPaginationHelpers is available during hoisting +const mocks = vi.hoisted(() => { + return import('../../../utils/mocks/core'); +}); + +vi.mock('../../../../src/utils/pagination/helpers', async () => (await mocks).mockPaginationHelpers); + +// ===== TEST SUITE ===== +describe('ProcessInstancesService', () => { + let service: ProcessInstancesService; + let mockApiClient: any; + + beforeEach(async () => { + // Create mock instances using centralized setup + const { config, executionContext, tokenManager } = createServiceTestDependencies(); + mockApiClient = createMockApiClient(); + + // Mock the ApiClient constructor + vi.mocked(ApiClient).mockImplementation(() => mockApiClient); + + // Reset pagination helpers mock before each test + vi.mocked(PaginationHelpers.getAll).mockReset(); + + service = new ProcessInstancesService(config, executionContext, tokenManager); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('getAll', () => { + it('should return all process instances without pagination', async () => { + // Mock the pagination helper to return our test data + const mockResponse = { + items: [createMockProcessInstance()], + totalCount: 1, + nextPage: null + }; + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const result = await service.getAll(); + + // Verify PaginationHelpers.getAll was called with correct parameters + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.any(Function), + transformFn: expect.any(Function), + pagination: expect.any(Object) + }), + undefined + ); + + expect(result).toEqual(mockResponse); + }); + + it('should return paginated process instances when pagination options provided', async () => { + // Mock the pagination helper to return our test data + const mockResponse = { + items: [createMockProcessInstance()], + totalCount: 1, + nextPage: 'nextPage' + }; + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const options: ProcessInstanceGetAllWithPaginationOptions = { + pageSize: TEST_CONSTANTS.PAGE_SIZE, + cursor: { + value: 'test-cursor-value' + } + }; + + const result = await service.getAll(options); + + // Verify PaginationHelpers.getAll was called with correct parameters + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.any(Function), + transformFn: expect.any(Function), + pagination: expect.any(Object) + }), + expect.objectContaining({ + pageSize: TEST_CONSTANTS.PAGE_SIZE, + cursor: expect.objectContaining({ + value: 'test-cursor-value' + }) + }) + ); + + expect(result).toEqual(mockResponse); + }); + + it('should handle filtering options', async () => { + // Mock the pagination helper to return our test data + const mockResponse = { + items: [], + totalCount: 0, + nextPage: null + }; + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const options: ProcessInstanceGetAllWithPaginationOptions = { + processKey: MAESTRO_TEST_CONSTANTS.PROCESS_KEY, + packageId: MAESTRO_TEST_CONSTANTS.PACKAGE_ID, + errorCode: MAESTRO_TEST_CONSTANTS.ERROR_CODE + }; + + await service.getAll(options); + + // Verify PaginationHelpers.getAll was called with correct parameters + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.any(Function), + transformFn: expect.any(Function), + pagination: expect.any(Object) + }), + expect.objectContaining({ + processKey: MAESTRO_TEST_CONSTANTS.PROCESS_KEY, + packageId: MAESTRO_TEST_CONSTANTS.PACKAGE_ID, + errorCode: MAESTRO_TEST_CONSTANTS.ERROR_CODE + }) + ); + }); + + it('should handle API errors', async () => { + // Mock the pagination helper to throw an error + vi.mocked(PaginationHelpers.getAll).mockRejectedValue(new Error(TEST_CONSTANTS.ERROR_MESSAGE)); + + await expect(service.getAll()).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); + + describe('getById', () => { + it('should return process instance by ID with operation methods', async () => { + + const instanceId = MAESTRO_TEST_CONSTANTS.INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + const mockApiResponse: RawProcessInstanceGetResponse = createMockProcessInstance(); + + mockApiClient.get.mockResolvedValue(mockApiResponse); + + + const result = await service.getById(instanceId, folderKey); + + + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.INSTANCES.GET_BY_ID(instanceId), + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey + }) + } + ); + + expect(result).toHaveProperty('instanceId', MAESTRO_TEST_CONSTANTS.INSTANCE_ID); + expect(result).toHaveProperty('cancel'); + expect(result).toHaveProperty('pause'); + expect(result).toHaveProperty('resume'); + expect(result).toHaveProperty('getExecutionHistory'); + expect(result).toHaveProperty('getBpmn'); + expect(result).toHaveProperty('getVariables'); + }); + + it('should handle API errors', async () => { + + const error = new Error('API Error'); + mockApiClient.get.mockRejectedValue(error); + + + await expect(service.getById(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow('API Error'); + }); + }); + + + describe('getExecutionHistory', () => { + it('should return execution history for process instance', async () => { + + const instanceId = MAESTRO_TEST_CONSTANTS.INSTANCE_ID; + const mockApiResponse: ProcessInstanceExecutionHistoryResponse[] = [createMockExecutionHistory()]; + + mockApiClient.get.mockResolvedValue(mockApiResponse); + + + const result = await service.getExecutionHistory(instanceId); + + + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.INSTANCES.GET_EXECUTION_HISTORY(instanceId), + {} + ); + + expect(result).toHaveLength(1); + expect(result[0]).toHaveProperty('id', MAESTRO_TEST_CONSTANTS.SPAN_ID); + expect(result[0]).toHaveProperty('traceId', MAESTRO_TEST_CONSTANTS.TRACE_ID); + }); + + it('should handle API errors', async () => { + + const error = new Error('API Error'); + mockApiClient.get.mockRejectedValue(error); + + + await expect(service.getExecutionHistory(MAESTRO_TEST_CONSTANTS.INSTANCE_ID)).rejects.toThrow('API Error'); + }); + }); + + describe('getBpmn', () => { + it('should return BPMN XML for process instance', async () => { + + const instanceId = MAESTRO_TEST_CONSTANTS.INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + const mockBpmnXml = createMockBpmnWithVariables(); + + mockApiClient.get.mockResolvedValue(mockBpmnXml); + + + const result = await service.getBpmn(instanceId, folderKey); + + + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.INSTANCES.GET_BPMN(instanceId), + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey, + 'Accept': CONTENT_TYPES.XML + }) + } + ); + + expect(result).toBe(mockBpmnXml); + }); + + it('should handle API errors', async () => { + + const error = new Error('API Error'); + mockApiClient.get.mockRejectedValue(error); + + + await expect(service.getBpmn(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow('API Error'); + }); + }); + + describe('cancel', () => { + it('should cancel process instance successfully', async () => { + + const instanceId = MAESTRO_TEST_CONSTANTS.INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + const options: ProcessInstanceOperationOptions = { + comment: MAESTRO_TEST_CONSTANTS.CANCEL_COMMENT + }; + const mockApiResponse = createMockMaestroApiOperationResponse({ + status: 'Cancelled' + }); + + mockApiClient.post.mockResolvedValue(mockApiResponse); + + + const result = await service.cancel(instanceId, folderKey, options); + + + expect(mockApiClient.post).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.INSTANCES.CANCEL(instanceId), + options, + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey + }) + } + ); + + expect(result.success).toBe(true); + expect(result.data).toEqual(mockApiResponse); + }); + + it('should cancel process instance without options', async () => { + + const instanceId = MAESTRO_TEST_CONSTANTS.INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + const mockApiResponse = createMockMaestroApiOperationResponse({ + status: 'Cancelled' + }); + + mockApiClient.post.mockResolvedValue(mockApiResponse); + + + const result = await service.cancel(instanceId, folderKey); + + + expect(mockApiClient.post).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.INSTANCES.CANCEL(instanceId), + {}, + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey + }) + } + ); + + expect(result.success).toBe(true); + expect(result.data).toEqual(mockApiResponse); + }); + + it('should handle API errors', async () => { + + const error = new Error('API Error'); + mockApiClient.post.mockRejectedValue(error); + + + await expect(service.cancel(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow('API Error'); + }); + }); + + describe('pause', () => { + it('should pause process instance successfully', async () => { + + const instanceId = MAESTRO_TEST_CONSTANTS.INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + const options: ProcessInstanceOperationOptions = { + comment: 'Pausing instance' + }; + const mockApiResponse = createMockMaestroApiOperationResponse({ + status: 'Paused' + }); + + mockApiClient.post.mockResolvedValue(mockApiResponse); + + + const result = await service.pause(instanceId, folderKey, options); + + + expect(mockApiClient.post).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.INSTANCES.PAUSE(instanceId), + options, + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey + }) + } + ); + + expect(result.success).toBe(true); + expect(result.data).toEqual(mockApiResponse); + }); + + it('should handle API errors', async () => { + + const error = new Error('API Error'); + mockApiClient.post.mockRejectedValue(error); + + + await expect(service.pause(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow('API Error'); + }); + }); + + describe('resume', () => { + it('should resume process instance successfully', async () => { + + const instanceId = MAESTRO_TEST_CONSTANTS.INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + const options: ProcessInstanceOperationOptions = { + comment: 'Resuming instance' + }; + const mockApiResponse = createMockMaestroApiOperationResponse({ + status: 'Running' + }); + + mockApiClient.post.mockResolvedValue(mockApiResponse); + + + const result = await service.resume(instanceId, folderKey, options); + + + expect(mockApiClient.post).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.INSTANCES.RESUME(instanceId), + options, + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey + }) + } + ); + + expect(result.success).toBe(true); + expect(result.data).toEqual(mockApiResponse); + }); + + it('should handle API errors', async () => { + + const error = new Error('API Error'); + mockApiClient.post.mockRejectedValue(error); + + + await expect(service.resume(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow('API Error'); + }); + }); + + describe('getVariables', () => { + it('should return variables for process instance with BPMN metadata', async () => { + + const instanceId = MAESTRO_TEST_CONSTANTS.INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + const mockBpmnXml = createMockBpmnWithVariables({ + elementId: MAESTRO_TEST_CONSTANTS.START_EVENT_ID, + elementName: MAESTRO_TEST_CONSTANTS.START_EVENT_NAME, + variableId: MAESTRO_TEST_CONSTANTS.VARIABLE_ID, + variableName: MAESTRO_TEST_CONSTANTS.VARIABLE_NAME + }); + + const mockVariablesResponse = createMockProcessVariables({ + globals: { + [MAESTRO_TEST_CONSTANTS.VARIABLE_ID]: MAESTRO_TEST_CONSTANTS.VARIABLE_VALUE + } + }); + + mockApiClient.get + .mockResolvedValueOnce(mockBpmnXml) // First call for BPMN + .mockResolvedValueOnce(mockVariablesResponse); // Second call for variables + + + const result = await service.getVariables(instanceId, folderKey); + + + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.INSTANCES.GET_BPMN(instanceId), + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey, + 'Accept': CONTENT_TYPES.XML + }) + } + ); + + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.INSTANCES.GET_VARIABLES(instanceId), + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey + }), + params: undefined + } + ); + + expect(result).toHaveProperty('instanceId', MAESTRO_TEST_CONSTANTS.INSTANCE_ID); + expect(result).toHaveProperty('elements'); + expect(result).toHaveProperty('globalVariables'); + expect(result.globalVariables).toHaveLength(1); + expect(result.globalVariables[0]).toMatchObject({ + id: MAESTRO_TEST_CONSTANTS.VARIABLE_ID, + name: MAESTRO_TEST_CONSTANTS.VARIABLE_NAME, + type: MAESTRO_TEST_CONSTANTS.VARIABLE_TYPE, + elementId: MAESTRO_TEST_CONSTANTS.START_EVENT_ID, + source: MAESTRO_TEST_CONSTANTS.START_EVENT_NAME, + value: MAESTRO_TEST_CONSTANTS.VARIABLE_VALUE + }); + }); + + it('should return variables with parentElementId filter', async () => { + + const instanceId = MAESTRO_TEST_CONSTANTS.INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + const options: ProcessInstanceGetVariablesOptions = { + parentElementId: 'parent-element-123' + }; + + const mockVariablesResponse = createMockProcessVariables({ + globals: {}, + parentElementId: 'parent-element-123' + }); + + mockApiClient.get + .mockResolvedValueOnce('') + .mockResolvedValueOnce(mockVariablesResponse); + + + const result = await service.getVariables(instanceId, folderKey, options); + + + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.INSTANCES.GET_VARIABLES(instanceId), + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey + }), + params: { + parentElementId: 'parent-element-123' + } + } + ); + + expect(result.parentElementId).toBe('parent-element-123'); + }); + + it('should handle BPMN fetch failure gracefully', async () => { + + const instanceId = MAESTRO_TEST_CONSTANTS.INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + const mockVariablesResponse = createMockProcessVariables({ + globals: { + [MAESTRO_TEST_CONSTANTS.VARIABLE_ID]: MAESTRO_TEST_CONSTANTS.VARIABLE_VALUE + } + }); + + // Mock console.warn to avoid test output + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + mockApiClient.get + .mockRejectedValueOnce(new Error('BPMN fetch failed')) // First call fails + .mockResolvedValueOnce(mockVariablesResponse); // Second call succeeds + + + const result = await service.getVariables(instanceId, folderKey); + + + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Failed to fetch BPMN metadata'), + expect.any(Error) + ); + + expect(result).toHaveProperty('instanceId', MAESTRO_TEST_CONSTANTS.INSTANCE_ID); + expect(result.globalVariables).toHaveLength(0); // No metadata available + + consoleSpy.mockRestore(); + }); + + it('should handle API errors', async () => { + + const error = new Error('API Error'); + mockApiClient.get.mockRejectedValue(error); + + + await expect(service.getVariables(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow('API Error'); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/services/maestro/processes.test.ts b/tests/unit/services/maestro/processes.test.ts new file mode 100644 index 00000000..d1d300c2 --- /dev/null +++ b/tests/unit/services/maestro/processes.test.ts @@ -0,0 +1,155 @@ +// ===== IMPORTS ===== +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { MaestroProcessesService } from '../../../../src/services/maestro/processes'; +import { MAESTRO_ENDPOINTS } from '../../../../src/utils/constants/endpoints'; +import { ApiClient } from '../../../../src/core/http/api-client'; +import { + MAESTRO_TEST_CONSTANTS, + createMockProcess, + createMockProcessesApiResponse, + createMockError +} from '../../../utils/mocks'; +import { createServiceTestDependencies, createMockApiClient } from '../../../utils/setup'; + +// ===== MOCKING ===== +// Mock the dependencies +vi.mock('../../../../src/core/http/api-client'); + +// ===== TEST SUITE ===== +describe('MaestroProcessesService', () => { + let service: MaestroProcessesService; + let mockApiClient: any; + + beforeEach(async () => { + // Create mock instances using centralized setup + const { config, executionContext, tokenManager } = createServiceTestDependencies(); + mockApiClient = createMockApiClient(); + + // Mock the ApiClient constructor + vi.mocked(ApiClient).mockImplementation(() => mockApiClient); + + service = new MaestroProcessesService(config, executionContext, tokenManager); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('getAll', () => { + it('should return all processes with instance statistics', async () => { + + const mockApiResponse = createMockProcessesApiResponse([ + createMockProcess(), + createMockProcess({ + processKey: MAESTRO_TEST_CONSTANTS.PROCESS_KEY_2, + packageId: MAESTRO_TEST_CONSTANTS.PACKAGE_ID_2, + name: MAESTRO_TEST_CONSTANTS.PACKAGE_ID_2 + }) + ]); + mockApiClient.get.mockResolvedValue(mockApiResponse); + + + const result = await service.getAll(); + + + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.PROCESSES.GET_ALL, + {} + ); + + expect(result).toHaveLength(2); + expect(result[0]).toMatchObject({ + processKey: MAESTRO_TEST_CONSTANTS.PROCESS_KEY, + packageId: MAESTRO_TEST_CONSTANTS.PACKAGE_ID, + name: MAESTRO_TEST_CONSTANTS.PACKAGE_ID, // name should be set to packageId + folderKey: MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + folderName: 'Test Folder' + }); + + expect(result[1]).toMatchObject({ + processKey: MAESTRO_TEST_CONSTANTS.PROCESS_KEY_2, + packageId: MAESTRO_TEST_CONSTANTS.PACKAGE_ID_2, + name: MAESTRO_TEST_CONSTANTS.PACKAGE_ID_2, + folderKey: MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + folderName: 'Test Folder' + }); + }); + + it('should handle empty processes array', async () => { + + const mockApiResponse = { processes: [] }; + mockApiClient.get.mockResolvedValue(mockApiResponse); + + + const result = await service.getAll(); + + + expect(result).toEqual([]); + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.PROCESSES.GET_ALL, + {} + ); + }); + + it('should handle undefined processes in response', async () => { + + const mockApiResponse = {}; + mockApiClient.get.mockResolvedValue(mockApiResponse); + + + const result = await service.getAll(); + + + expect(result).toEqual([]); + }); + + it('should handle response without processes property', async () => { + + const mockApiResponse = { + // Response has data but no processes property + someOtherProperty: MAESTRO_TEST_CONSTANTS.OTHER_PROPERTY, + metadata: { count: 0 } + }; + mockApiClient.get.mockResolvedValue(mockApiResponse); + + + const result = await service.getAll(); + + + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.PROCESSES.GET_ALL, + {} + ); + expect(result).toEqual([]); + expect(Array.isArray(result)).toBe(true); + expect(result).toHaveLength(0); + }); + + it('should handle API errors', async () => { + + const error = createMockError('API Error'); + mockApiClient.get.mockRejectedValue(error); + + + await expect(service.getAll()).rejects.toThrow('API Error'); + }); + + it('should set name field to packageId for each process', async () => { + + const mockApiResponse = createMockProcessesApiResponse([ + createMockProcess({ + processKey: MAESTRO_TEST_CONSTANTS.CUSTOM_PROCESS_KEY, + packageId: MAESTRO_TEST_CONSTANTS.CUSTOM_PACKAGE_ID + }) + ]); + + mockApiClient.get.mockResolvedValue(mockApiResponse); + + + const result = await service.getAll(); + + + expect(result[0].name).toBe(MAESTRO_TEST_CONSTANTS.CUSTOM_PACKAGE_ID); + }); + }); +}); \ No newline at end of file diff --git a/tests/utils/constants/common.ts b/tests/utils/constants/common.ts new file mode 100644 index 00000000..79a11ebd --- /dev/null +++ b/tests/utils/constants/common.ts @@ -0,0 +1,24 @@ +/** + * Common test constants used across all services + */ + +export const TEST_CONSTANTS = { + // Basic identifiers + USER_ID: 123, + FOLDER_ID: 123, + FOLDER_NAME: 'Test Folder', + // Common status values + RUNNING: 'Running', + CANCELLED: 'Cancelled', + + // Common values + PAGE_SIZE: 10, + ERROR_MESSAGE: 'API Error', + + // Base URLs and Endpoints + BASE_URL: 'https://test.uipath.com', + CLIENT_ID: 'test-client-id', + CLIENT_SECRET: 'test-client-secret', + ORGANIZATION_ID: 'test-org-id', + TENANT_ID: 'test-tenant-id', +} as const; \ No newline at end of file diff --git a/tests/utils/constants/index.ts b/tests/utils/constants/index.ts new file mode 100644 index 00000000..8b3be0eb --- /dev/null +++ b/tests/utils/constants/index.ts @@ -0,0 +1,6 @@ +/** + * Centralized exports for all test constants + */ + +export * from './common'; +export * from './maestro'; \ No newline at end of file diff --git a/tests/utils/constants/maestro.ts b/tests/utils/constants/maestro.ts new file mode 100644 index 00000000..e927db3f --- /dev/null +++ b/tests/utils/constants/maestro.ts @@ -0,0 +1,52 @@ +/** + * Maestro service test constants + * Maestro-specific constants only + */ + +export const MAESTRO_TEST_CONSTANTS = { + // Maestro-specific identifiers + PROCESS_KEY: 'TestProcess', + PACKAGE_ID: 'TestPackage', + PACKAGE_KEY: 'package-1', + FOLDER_KEY: 'test-folder-key', + INSTANCE_ID: 'instance-123', + RUN_ID: 'run-1', + PACKAGE_VERSION: '1.0.0', + MANUAL_SOURCE: 'Manual', + ATTRIBUTES: '{"key": "value"}', + + // Maestro-specific test data + INSTANCE_DISPLAY_NAME: 'Test Instance', + STARTED_BY_USER: 'user1', + CREATOR_USER_KEY: 'user1', + + SPAN_ID: 'span-1', + TRACE_ID: 'trace-1', + ACTIVITY_NAME: 'Activity1', + + + PARENT_ELEMENT_ID: 'parent-1', + + ERROR_CODE: 'TestError', + + CANCEL_COMMENT: 'Cancelling instance', + + START_TIME: '2023-01-01T10:00:00Z', + END_TIME: '2023-01-01T10:05:00Z', + + PROCESS_KEY_2: 'process-2', + PACKAGE_ID_2: 'package-2', + CUSTOM_PROCESS_KEY: 'custom-process', + CUSTOM_PACKAGE_ID: 'custom-package', + OTHER_PROPERTY: 'value', + + // Variable types + VARIABLE_TYPE: 'string', + + // BPMN element constants + START_EVENT_ID: 'start1', + START_EVENT_NAME: 'Start', + VARIABLE_ID: 'var1', + VARIABLE_NAME: 'Input Variable', + VARIABLE_VALUE: 'test value', +} as const; \ No newline at end of file diff --git a/tests/utils/mocks/core.ts b/tests/utils/mocks/core.ts index e69de29b..c27753a6 100644 --- a/tests/utils/mocks/core.ts +++ b/tests/utils/mocks/core.ts @@ -0,0 +1,83 @@ +/** + * Core mock utilities - Generic mocks used across all services + */ +import { vi } from 'vitest'; +import { TEST_CONSTANTS } from '../constants/common'; + +/** + * Generic factory for creating mock base response objects + * Use this as a building block for service-specific responses + * + * @param baseFields - Base fields common to most responses + * @param overrides - Additional or override fields + * @returns Mock response object + * + * @example + * ```typescript + * const mockResponse = createMockBaseResponse( + * { id: 'test', name: 'Test' }, + * { customField: 'value' } + * ); + * ``` + */ +export const createMockBaseResponse = >( + baseFields: T, + overrides: Partial = {} +): T => { + return { + ...baseFields, + ...overrides, + }; +}; + +// Error mocks +export const createMockError = (message: string = TEST_CONSTANTS.ERROR_MESSAGE) => { + const error = new Error(message) as any; + error.status = 500; + error.response = { status: 500, data: { message } }; + return error; +}; + +/** + * Creates a mock operation response that matches the OperationResponse interface + * Used for method responses that wrap API responses + * @param data - The data to wrap in the operation response + * @returns Mock operation response object with success and data fields + * + * @example + * ```typescript + * // For single item response + * const mockResponse = createMockOperationResponse({ id: '123', status: 'success' }); + * + * // For array response + * const mockResponse = createMockOperationResponse([{ taskId: '123', userId: '456' }]); + * ``` + */ +export const createMockOperationResponse = (data: T): { success: boolean; data: T } => ({ + success: true, + data +}); + +/** + * Pagination helpers mock - Used across all services that need pagination + * This provides a consistent mock for PaginationHelpers across all test files + * + * Usage in test files: + * ```typescript + * // Use vi.hoisted to ensure mockPaginationHelpers is available during hoisting + * const mocks = vi.hoisted(() => { + * return import('../../../utils/mocks/core'); + * }); + * + * vi.mock('../../../../src/utils/pagination/helpers', async () => (await mocks).mockPaginationHelpers); + * ``` + */ +export const mockPaginationHelpers = { + PaginationHelpers: { + getAll: vi.fn(), + hasPaginationParameters: vi.fn((options = {}) => { + const { cursor, pageSize, jumpToPage } = options; + return cursor !== undefined || pageSize !== undefined || jumpToPage !== undefined; + }) + } +}; diff --git a/tests/utils/mocks/index.ts b/tests/utils/mocks/index.ts new file mode 100644 index 00000000..22cea9b7 --- /dev/null +++ b/tests/utils/mocks/index.ts @@ -0,0 +1,13 @@ +/** + * Centralized exports for all mock utilities + * Single entry point for all test mocks + */ + +// Core mock utilities (generic) +export * from './core'; + +// Service-specific mock utilities +export * from './maestro'; + +// Re-export constants for convenience +export * from '../constants'; \ No newline at end of file diff --git a/tests/utils/mocks/maestro.ts b/tests/utils/mocks/maestro.ts new file mode 100644 index 00000000..4ba9d546 --- /dev/null +++ b/tests/utils/mocks/maestro.ts @@ -0,0 +1,170 @@ +/** + * Maestro service mock utilities - Maestro-specific mocks only + * Uses generic utilities from core.ts for base functionality + */ + +import { TEST_CONSTANTS } from '../constants/common'; +import { MAESTRO_TEST_CONSTANTS } from '../constants/maestro'; +import { createMockBaseResponse } from './core'; + +// Maestro-Specific Mock Factories + +/** + * Creates a mock Process object + * @param overrides - Optional overrides for specific fields + * @returns Mock Process object + */ +export const createMockProcess = (overrides: Partial = {}) => { + return createMockBaseResponse({ + processKey: MAESTRO_TEST_CONSTANTS.PROCESS_KEY, + packageId: MAESTRO_TEST_CONSTANTS.PACKAGE_ID, + name: MAESTRO_TEST_CONSTANTS.PACKAGE_ID, + folderKey: MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + folderName: TEST_CONSTANTS.FOLDER_NAME, + packageVersions: [MAESTRO_TEST_CONSTANTS.PACKAGE_VERSION], + versionCount: 1, + pendingCount: 0, + runningCount: 1, + completedCount: 0, + pausedCount: 0, + cancelledCount: 0, + faultedCount: 0, + retryingCount: 0, + resumingCount: 0, + pausingCount: 0, + cancelingCount: 0, + }, overrides); +}; + +/** + * Creates a mock Process Instance object + * @param overrides - Optional overrides for specific fields + * @returns Mock Process Instance object + */ +export const createMockProcessInstance = (overrides: Partial = {}) => { + return createMockBaseResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + packageKey: MAESTRO_TEST_CONSTANTS.PACKAGE_KEY, + packageId: MAESTRO_TEST_CONSTANTS.PACKAGE_ID, + packageVersion: MAESTRO_TEST_CONSTANTS.PACKAGE_VERSION, + latestRunId: MAESTRO_TEST_CONSTANTS.RUN_ID, + latestRunStatus: TEST_CONSTANTS.RUNNING, + processKey: MAESTRO_TEST_CONSTANTS.PROCESS_KEY, + folderKey: MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + userId: TEST_CONSTANTS.USER_ID, + instanceDisplayName: MAESTRO_TEST_CONSTANTS.INSTANCE_DISPLAY_NAME, + startedByUser: MAESTRO_TEST_CONSTANTS.STARTED_BY_USER, + source: MAESTRO_TEST_CONSTANTS.MANUAL_SOURCE, + creatorUserKey: MAESTRO_TEST_CONSTANTS.CREATOR_USER_KEY, + startedTime: new Date().toISOString(), + completedTime: null, + instanceRuns: [], + }, overrides); +}; + + +// API Response Mocks + +/** + * Creates a mock Processes API response + * @param processes - Array of processes (optional) + * @returns Mock API response for processes + */ +export const createMockProcessesApiResponse = (processes: any[] = []) => { + return createMockBaseResponse({ + processes: processes.length > 0 ? processes : [createMockProcess()] + }); +}; + +/** + * Creates a mock Process Instance Execution History entry + * @param overrides - Optional overrides for specific fields + * @returns Mock Execution History object + */ +export const createMockExecutionHistory = (overrides: Partial = {}) => { + return createMockBaseResponse({ + id: MAESTRO_TEST_CONSTANTS.SPAN_ID, + traceId: MAESTRO_TEST_CONSTANTS.TRACE_ID, + parentId: null, + name: MAESTRO_TEST_CONSTANTS.ACTIVITY_NAME, + startedTime: new Date().toISOString(), + endTime: new Date().toISOString(), + attributes: MAESTRO_TEST_CONSTANTS.ATTRIBUTES, + createdTime: new Date().toISOString(), + updatedTime: new Date().toISOString(), + expiredTime: null + }, overrides); +}; + +/** + * Creates a mock Process Instance Variables response + * @param overrides - Optional overrides for specific fields + * @returns Mock Variables response object + */ +export const createMockProcessVariables = (overrides: Partial = {}) => { + return createMockBaseResponse({ + elements: [], + globals: { + [MAESTRO_TEST_CONSTANTS.VARIABLE_ID]: MAESTRO_TEST_CONSTANTS.VARIABLE_VALUE + }, + instanceId: MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + parentElementId: null + }, overrides); +}; + +/** + * Creates a mock BPMN XML with variables + * @param overrides - Optional overrides for specific fields + * @returns Mock BPMN XML string + */ +/** + * Creates a mock API operation response (for cancel/pause/resume operations) + * @param overrides - Optional overrides for specific fields + * @returns Mock operation response object + */ +/** + * Creates a mock Maestro operation response + * @param overrides - Optional overrides for specific fields + * @param wrapInOperationResponse - If true, wraps the response in OperationResponse format (for method responses) + * @returns Mock operation response object + * + * @example + * ```typescript + * // For API response (in service tests) + * const mockApiResponse = createMockMaestroApiOperationResponse(); + * + * // For method response (in model tests) + * const mockMethodResponse = createMockMaestroApiOperationResponse({}, true); + * ``` + */ +export const createMockMaestroApiOperationResponse = (overrides: Partial = {}) => { + const apiResponse = createMockBaseResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + status: TEST_CONSTANTS.RUNNING + }, overrides); + + return apiResponse; +}; + +export const createMockBpmnWithVariables = (overrides: Partial = {}) => { + const defaults = { + processId: MAESTRO_TEST_CONSTANTS.PROCESS_KEY, + processName: MAESTRO_TEST_CONSTANTS.INSTANCE_DISPLAY_NAME, + elementId: MAESTRO_TEST_CONSTANTS.PARENT_ELEMENT_ID, + elementName: MAESTRO_TEST_CONSTANTS.ACTIVITY_NAME, + variableId: MAESTRO_TEST_CONSTANTS.SPAN_ID, + variableName: `${MAESTRO_TEST_CONSTANTS.ACTIVITY_NAME} Variable`, + variableType: MAESTRO_TEST_CONSTANTS.VARIABLE_TYPE + }; + + const config = { ...defaults, ...overrides }; + + return ` + + + + + + + `; +}; diff --git a/tests/utils/setup.ts b/tests/utils/setup.ts index e69de29b..ca2e2143 100644 --- a/tests/utils/setup.ts +++ b/tests/utils/setup.ts @@ -0,0 +1,91 @@ +import { vi } from 'vitest'; +import { Config } from '../../src/core/config/config'; +import { ExecutionContext } from '../../src/core/context/execution'; +import { TokenManager } from '../../src/core/auth/token-manager'; + +// Mock console methods to avoid test output noise +global.console = { + ...console, + warn: vi.fn(), + error: vi.fn(), + log: vi.fn(), + info: vi.fn(), + debug: vi.fn() +}; + +// Mock environment variables +process.env.NODE_ENV = 'test'; + +import { TEST_CONSTANTS } from './constants/common'; + +/** + * Creates a mock Config object for testing + * @param overrides - Optional overrides for specific config values + * @returns Mock Config object + */ +export const createMockConfig = (overrides?: Partial): Config => { + return { + baseUrl: TEST_CONSTANTS.BASE_URL, + clientId: TEST_CONSTANTS.CLIENT_ID, + clientSecret: TEST_CONSTANTS.CLIENT_SECRET, + organizationId: TEST_CONSTANTS.ORGANIZATION_ID, + tenantId: TEST_CONSTANTS.TENANT_ID, + ...overrides, + } as Config; +}; + +/** + * Creates a mock ExecutionContext for testing + * @returns Mock ExecutionContext instance + */ +export const createMockExecutionContext = (): ExecutionContext => { + return new ExecutionContext(); +}; + +/** + * Creates a mock TokenManager for testing + * @param overrides - Optional overrides for specific methods + * @returns Mock TokenManager object + */ +export const createMockTokenManager = (overrides?: Partial): TokenManager => { + return { + getToken: vi.fn().mockReturnValue('mock-access-token'), + hasValidToken: vi.fn().mockReturnValue(true), + ...overrides, + } as unknown as TokenManager; +}; + +/** + * Mock ApiClient factory + * @returns Mock ApiClient object + */ +export const createMockApiClient = () => ({ + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + patch: vi.fn(), + delete: vi.fn() +}); + +/** + * Creates all common service test dependencies at once + * @param configOverrides - Optional config overrides + * @param tokenManagerOverrides - Optional token manager overrides + * @returns Object containing all common mocks + * + * @example + * ```typescript + * const { config, executionContext, tokenManager } = createServiceTestDependencies(); + * const service = new MyService(config, executionContext, tokenManager); + * ``` + */ +export const createServiceTestDependencies = ( + configOverrides?: Partial, + tokenManagerOverrides?: Partial +) => { + return { + config: createMockConfig(configOverrides), + executionContext: createMockExecutionContext(), + tokenManager: createMockTokenManager(tokenManagerOverrides), + }; +}; \ No newline at end of file From 1d5d745fee40a7862394316b3461c40b2ba2f3d5 Mon Sep 17 00:00:00 2001 From: swati354 <103816801+swati354@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:32:36 +0530 Subject: [PATCH 3/7] Add unit tests for Tasks service (#100) --- src/models/action-center/tasks.types.ts | 11 +- tests/unit/models/action-center/index.ts | 5 + tests/unit/models/action-center/tasks.test.ts | 304 ++++++ tests/unit/services/action-center/index.ts | 5 + .../unit/services/action-center/tasks.test.ts | 885 ++++++++++++++++++ tests/unit/services/tasks.test.ts | 0 tests/utils/constants/index.ts | 4 +- tests/utils/constants/tasks.ts | 58 ++ tests/utils/mocks/core.ts | 68 +- tests/utils/mocks/index.ts | 1 + tests/utils/mocks/tasks.ts | 148 +++ 11 files changed, 1466 insertions(+), 23 deletions(-) create mode 100644 tests/unit/models/action-center/index.ts create mode 100644 tests/unit/models/action-center/tasks.test.ts create mode 100644 tests/unit/services/action-center/index.ts create mode 100644 tests/unit/services/action-center/tasks.test.ts delete mode 100644 tests/unit/services/tasks.test.ts create mode 100644 tests/utils/constants/tasks.ts create mode 100644 tests/utils/mocks/tasks.ts diff --git a/src/models/action-center/tasks.types.ts b/src/models/action-center/tasks.types.ts index dfa2c205..cdb1cf21 100644 --- a/src/models/action-center/tasks.types.ts +++ b/src/models/action-center/tasks.types.ts @@ -94,7 +94,7 @@ export interface TaskActivity { activityType: TaskActivityType; creatorUserId: number; targetUserId: number | null; - creationTime: string; + createdTime: string; } export interface TaskSlaDetail { @@ -121,20 +121,21 @@ export interface TaskBaseResponse { folderId: number; key: string; isDeleted: boolean; - creationTime: string; + createdTime: string; id: number; action: string | null; externalTag: string | null; lastAssignedTime: string | null; - completionTime: string | null; + completedTime: string | null; parentOperationId: string | null; deleterUserId: number | null; - deletionTime: string | null; - lastModificationTime: string | null; + deletedTime: string | null; + lastModifiedTime: string | null; } export interface TaskCreateOptions { title: string; + data?: Record; priority?: TaskPriority; } diff --git a/tests/unit/models/action-center/index.ts b/tests/unit/models/action-center/index.ts new file mode 100644 index 00000000..96bac153 --- /dev/null +++ b/tests/unit/models/action-center/index.ts @@ -0,0 +1,5 @@ +/** + * Action Center models test exports + */ + +export * from './tasks.test'; \ No newline at end of file diff --git a/tests/unit/models/action-center/tasks.test.ts b/tests/unit/models/action-center/tasks.test.ts new file mode 100644 index 00000000..29002549 --- /dev/null +++ b/tests/unit/models/action-center/tasks.test.ts @@ -0,0 +1,304 @@ +// ===== IMPORTS ===== +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { createTaskWithMethods } from '../../../../src/models/action-center/tasks.models'; +import type { TaskServiceModel } from '../../../../src/models/action-center/tasks.models'; +import { TaskType, TaskPriority} from '../../../../src/models/action-center/tasks.types'; +import { createBasicTask } from '../../../utils/mocks/tasks'; +import { createMockOperationResponse } from '../../../utils/mocks/core'; +import { TASK_TEST_CONSTANTS } from '../../../utils/constants/tasks'; +import { TEST_CONSTANTS } from '../../../utils/constants/common'; + +// ===== TEST SUITE ===== +describe('Task Models', () => { + let mockService: TaskServiceModel; + + beforeEach(() => { + // Create a mock service + mockService = { + assign: vi.fn(), + reassign: vi.fn(), + unassign: vi.fn(), + complete: vi.fn(), + create: vi.fn(), + getAll: vi.fn(), + getById: vi.fn(), + getUsers: vi.fn(), + } as any; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('bound methods on task', () => { + describe('task.assign()', () => { + it('should call task.assign with userId', async () => { + const taskData = createBasicTask(); + const task = createTaskWithMethods(taskData, mockService); + + const mockResponse = createMockOperationResponse([ + { taskId: TASK_TEST_CONSTANTS.TASK_ID, userId: TASK_TEST_CONSTANTS.USER_ID } + ]); + mockService.assign = vi.fn().mockResolvedValue(mockResponse); + + + const result = await task.assign({ userId: TASK_TEST_CONSTANTS.USER_ID }); + + + expect(mockService.assign).toHaveBeenCalledWith({ + taskId: TASK_TEST_CONSTANTS.TASK_ID, + userId: TASK_TEST_CONSTANTS.USER_ID + }); + expect(result).toEqual(mockResponse); + }); + + it('should call task.assign with userNameOrEmail', async () => { + const taskData = createBasicTask(); + const task = createTaskWithMethods(taskData, mockService); + + const mockResponse = createMockOperationResponse([ + { taskId: TASK_TEST_CONSTANTS.TASK_ID, userNameOrEmail: TASK_TEST_CONSTANTS.USER_EMAIL } + ]); + mockService.assign = vi.fn().mockResolvedValue(mockResponse); + + + const result = await task.assign({ userNameOrEmail: TASK_TEST_CONSTANTS.USER_EMAIL }); + + + expect(mockService.assign).toHaveBeenCalledWith({ + taskId: TASK_TEST_CONSTANTS.TASK_ID, + userNameOrEmail: TASK_TEST_CONSTANTS.USER_EMAIL + }); + expect(result).toEqual(mockResponse); + }); + + it('should throw error if taskId is undefined', async () => { + const taskData = createBasicTask({ id: undefined }); + const task = createTaskWithMethods(taskData, mockService); + + + await expect(task.assign({ userId: TASK_TEST_CONSTANTS.USER_ID })).rejects.toThrow('Task ID is undefined'); + }); + }); + + describe('task.reassign()', () => { + it('should call task.reassign with userId', async () => { + const taskData = createBasicTask(); + const task = createTaskWithMethods(taskData, mockService); + + const mockResponse = createMockOperationResponse([ + { taskId: TASK_TEST_CONSTANTS.TASK_ID, userId: TASK_TEST_CONSTANTS.USER_ID } + ]); + mockService.reassign = vi.fn().mockResolvedValue(mockResponse); + + + const result = await task.reassign({ userId: TASK_TEST_CONSTANTS.USER_ID }); + + + expect(mockService.reassign).toHaveBeenCalledWith({ + taskId: TASK_TEST_CONSTANTS.TASK_ID, + userId: TASK_TEST_CONSTANTS.USER_ID + }); + expect(result).toEqual(mockResponse); + }); + + it('should call task.reassign with userNameOrEmail', async () => { + const taskData = createBasicTask(); + const task = createTaskWithMethods(taskData, mockService); + + const mockResponse = createMockOperationResponse([ + { taskId: TASK_TEST_CONSTANTS.TASK_ID, userNameOrEmail: TASK_TEST_CONSTANTS.USER_EMAIL } + ]); + mockService.reassign = vi.fn().mockResolvedValue(mockResponse); + + + const result = await task.reassign({ userNameOrEmail: TASK_TEST_CONSTANTS.USER_EMAIL }); + + + expect(mockService.reassign).toHaveBeenCalledWith({ + taskId: TASK_TEST_CONSTANTS.TASK_ID, + userNameOrEmail: TASK_TEST_CONSTANTS.USER_EMAIL + }); + expect(result).toEqual(mockResponse); + }); + + it('should throw error if taskId is undefined', async () => { + const taskData = createBasicTask({ id: undefined }); + const task = createTaskWithMethods(taskData, mockService); + + + await expect(task.reassign({ userId: TASK_TEST_CONSTANTS.USER_ID })).rejects.toThrow('Task ID is undefined'); + }); + }); + + describe('task.unassign()', () => { + it('should call task.unassign', async () => { + const taskData = createBasicTask(); + const task = createTaskWithMethods(taskData, mockService); + + const mockResponse = createMockOperationResponse([ + { taskId: TASK_TEST_CONSTANTS.TASK_ID } + ]); + mockService.unassign = vi.fn().mockResolvedValue(mockResponse); + + + const result = await task.unassign(); + + + expect(mockService.unassign).toHaveBeenCalledWith(TASK_TEST_CONSTANTS.TASK_ID); + expect(result).toEqual(mockResponse); + }); + + it('should throw error if taskId is undefined', async () => { + const taskData = createBasicTask({ id: undefined }); + const task = createTaskWithMethods(taskData, mockService); + + + await expect(task.unassign()).rejects.toThrow('Task ID is undefined'); + }); + }); + + describe('task.complete()', () => { + it('should call task.complete for external task', async () => { + const taskData = createBasicTask({ folderId: TEST_CONSTANTS.FOLDER_ID, type: TaskType.External }); + const task = createTaskWithMethods(taskData, mockService); + + const mockResponse = createMockOperationResponse({ + type: TaskType.External, + taskId: TASK_TEST_CONSTANTS.TASK_ID + }); + mockService.complete = vi.fn().mockResolvedValue(mockResponse); + + + const result = await task.complete({ + type: TaskType.External + }); + + + expect(mockService.complete).toHaveBeenCalledWith( + { + type: TaskType.External, + taskId: TASK_TEST_CONSTANTS.TASK_ID, + data: undefined, + action: undefined + }, + TEST_CONSTANTS.FOLDER_ID + ); + expect(result).toEqual(mockResponse); + }); + + it('should call task.complete for app task', async () => { + const taskData = createBasicTask({ folderId: TEST_CONSTANTS.FOLDER_ID, type: TaskType.App }); + const task = createTaskWithMethods(taskData, mockService); + + const mockResponse = createMockOperationResponse({ + type: TaskType.App, + taskId: TASK_TEST_CONSTANTS.TASK_ID, + data: {}, + action: TASK_TEST_CONSTANTS.ACTION_APPROVE + }); + mockService.complete = vi.fn().mockResolvedValue(mockResponse); + + + const result = await task.complete({ + type: TaskType.App, + data: TASK_TEST_CONSTANTS.APP_TASK_DATA, + action: TASK_TEST_CONSTANTS.ACTION_APPROVE + }); + + + expect(mockService.complete).toHaveBeenCalledWith( + { + type: TaskType.App, + taskId: TASK_TEST_CONSTANTS.TASK_ID, + data: TASK_TEST_CONSTANTS.APP_TASK_DATA, + action: TASK_TEST_CONSTANTS.ACTION_APPROVE + }, + TEST_CONSTANTS.FOLDER_ID + ); + expect(result).toEqual(mockResponse); + }); + + it('should call task.complete for form task', async () => { + const taskData = createBasicTask({ folderId: TEST_CONSTANTS.FOLDER_ID, type: TaskType.Form }); + const task = createTaskWithMethods(taskData, mockService); + + const mockResponse = createMockOperationResponse({ + type: TaskType.Form, + taskId: TASK_TEST_CONSTANTS.TASK_ID, + data: TASK_TEST_CONSTANTS.FORM_DATA, + action: TASK_TEST_CONSTANTS.ACTION_SUBMIT + }); + mockService.complete = vi.fn().mockResolvedValue(mockResponse); + + + const result = await task.complete({ + type: TaskType.Form, + data: TASK_TEST_CONSTANTS.FORM_DATA, + action: TASK_TEST_CONSTANTS.ACTION_SUBMIT + }); + + + expect(mockService.complete).toHaveBeenCalledWith( + { + type: TaskType.Form, + taskId: TASK_TEST_CONSTANTS.TASK_ID, + data: TASK_TEST_CONSTANTS.FORM_DATA, + action: TASK_TEST_CONSTANTS.ACTION_SUBMIT + }, + TEST_CONSTANTS.FOLDER_ID + ); + expect(result).toEqual(mockResponse); + }); + + it('should throw error if taskId is undefined', async () => { + const taskData = createBasicTask({ id: undefined }); + const task = createTaskWithMethods(taskData, mockService); + + + await expect(task.complete({ + type: TaskType.External, + data: {}, + action: TASK_TEST_CONSTANTS.ACTION_SUBMIT + })).rejects.toThrow('Task ID is undefined'); + }); + + it('should throw error if folderId is undefined', async () => { + const taskData = createBasicTask({ folderId: undefined }); + const task = createTaskWithMethods(taskData, mockService); + + + await expect(task.complete({ + type: TaskType.External, + data: {}, + action: TASK_TEST_CONSTANTS.ACTION_SUBMIT + })).rejects.toThrow('Folder ID is required'); + }); + }); + }); + + describe('Task data and methods are combined correctly', () => { + it('should preserve all task properties', () => { + const taskData = createBasicTask(); + const task = createTaskWithMethods(taskData, mockService); + + expect(task.id).toBe(TASK_TEST_CONSTANTS.TASK_ID); + expect(task.title).toBe(TASK_TEST_CONSTANTS.TASK_TITLE); + expect(task.type).toBe(TaskType.External); + expect(task.priority).toBe(TaskPriority.Medium); + expect(task.folderId).toBe(TEST_CONSTANTS.FOLDER_ID); + expect(task.key).toBe(TASK_TEST_CONSTANTS.TASK_KEY); + }); + + it('should have all methods available', () => { + const taskData = createBasicTask(); + const task = createTaskWithMethods(taskData, mockService); + + expect(typeof task.assign).toBe('function'); + expect(typeof task.reassign).toBe('function'); + expect(typeof task.unassign).toBe('function'); + expect(typeof task.complete).toBe('function'); + }); + }); +}); + diff --git a/tests/unit/services/action-center/index.ts b/tests/unit/services/action-center/index.ts new file mode 100644 index 00000000..3d7e898e --- /dev/null +++ b/tests/unit/services/action-center/index.ts @@ -0,0 +1,5 @@ +/** + * Action Center services test exports + */ + +export * from './tasks.test'; \ No newline at end of file diff --git a/tests/unit/services/action-center/tasks.test.ts b/tests/unit/services/action-center/tasks.test.ts new file mode 100644 index 00000000..b3d2c885 --- /dev/null +++ b/tests/unit/services/action-center/tasks.test.ts @@ -0,0 +1,885 @@ +// ===== IMPORTS ===== +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { TaskService } from '../../../../src/services/action-center/tasks'; +import { + TaskType, + TaskPriority, + TaskAssignmentOptions, + TaskCompletionOptions, + TaskCreateOptions, + TaskGetAllOptions, + TaskGetUsersOptions +} from '../../../../src/models/action-center/tasks.types'; +import { PaginationHelpers } from '../../../../src/utils/pagination/helpers'; +import { ApiClient } from '../../../../src/core/http/api-client'; +import { createServiceTestDependencies, createMockApiClient } from '../../../utils/setup'; +import { + createMockTaskResponse, + createMockTaskGetResponse, + createMockTasks, + createMockUsers +} from '../../../utils/mocks/tasks'; +import { createMockError } from '../../../utils/mocks/core'; +import { DEFAULT_TASK_EXPAND } from '../../../../src/models/action-center/tasks.constants'; +import { TASK_TEST_CONSTANTS } from '../../../utils/constants/tasks'; +import { TEST_CONSTANTS } from '../../../utils/constants/common'; +import { TASK_ENDPOINTS } from '../../../../src/utils/constants/endpoints'; +import { FOLDER_ID } from '../../../../src/utils/constants/headers'; + +// ===== MOCKING ===== +// Mock the dependencies +vi.mock('../../../../src/core/http/api-client'); + +// Import mock objects using vi.hoisted() - this ensures they're available before vi.mock() calls +const mocks = vi.hoisted(() => { + // Import/re-export the mock utilities from core + return import('../../../utils/mocks/core'); +}); + +// Setup all mocks at module level +vi.mock('../../../../src/utils/transform', async () => (await mocks).mockTransformUtils); +vi.mock('../../../../src/utils/pagination/helpers', async () => (await mocks).mockPaginationHelpers); + +// ===== TEST SUITE ===== +describe('TaskService Unit Tests', () => { + let taskService: TaskService; + let mockApiClient: any; + + beforeEach(() => { + // Create mock instances using centralized setup + const { config, executionContext, tokenManager } = createServiceTestDependencies(); + mockApiClient = createMockApiClient(); + + // Mock the ApiClient constructor + vi.mocked(ApiClient).mockImplementation(() => mockApiClient); + + // Reset pagination helpers mock before each test + vi.mocked(PaginationHelpers.getAll).mockReset(); + + taskService = new TaskService(config, executionContext, tokenManager); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('create', () => { + it('should create a task successfully with all fields mapped correctly', async () => { + const taskInput = { + title: TASK_TEST_CONSTANTS.TASK_TITLE, + priority: TaskPriority.High + } as TaskCreateOptions; + + const mockResponse = createMockTaskResponse({ + title: TASK_TEST_CONSTANTS.TASK_TITLE, + priority: TaskPriority.High + }); + + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await taskService.create(taskInput, TEST_CONSTANTS.FOLDER_ID); + + // Verify the result + expect(result).toBeDefined(); + expect(result.title).toBe(TASK_TEST_CONSTANTS.TASK_TITLE); + expect(result.priority).toBe(TaskPriority.High); + + // Verify the API call has correct endpoint, body, and headers + expect(mockApiClient.post).toHaveBeenCalledWith( + TASK_ENDPOINTS.CREATE_GENERIC_TASK, + expect.objectContaining({ + title: TASK_TEST_CONSTANTS.TASK_TITLE, + priority: TaskPriority.High, + type: TaskType.External // SDK adds this automatically + }), + expect.objectContaining({ + headers: expect.objectContaining({ + [FOLDER_ID]: TEST_CONSTANTS.FOLDER_ID.toString() + }) + }) + ); + }); + + it('should handle optional data field with nested objects', async () => { + const taskInput = { + title: TASK_TEST_CONSTANTS.TASK_TITLE_COMPLEX, + priority: TaskPriority.Critical, + data: TASK_TEST_CONSTANTS.CUSTOM_DATA + } as TaskCreateOptions; + + const mockResponse = createMockTaskResponse({ + priority: TaskPriority.Critical, + data: taskInput.data + }); + + mockApiClient.post.mockResolvedValue(mockResponse); + + await taskService.create(taskInput, TEST_CONSTANTS.FOLDER_ID); + + // Verify complex data structures are passed through + expect(mockApiClient.post).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + data: TASK_TEST_CONSTANTS.CUSTOM_DATA + }), + expect.any(Object) + ); + }); + + it('should handle API errors', async () => { + const taskInput = { + title: TASK_TEST_CONSTANTS.TASK_TITLE, + priority: TaskPriority.High + } as TaskCreateOptions; + + const error = createMockError(TEST_CONSTANTS.ERROR_MESSAGE); + mockApiClient.post.mockRejectedValue(error); + + await expect(taskService.create(taskInput, TEST_CONSTANTS.FOLDER_ID)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); + + describe('assign', () => { + it('should assign a single task successfully', async () => { + const assignment = { + taskId: TASK_TEST_CONSTANTS.TASK_ID, + userId: TASK_TEST_CONSTANTS.USER_ID + } as TaskAssignmentOptions; + + const mockResponse = { + value: [] // Empty array means success + }; + + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await taskService.assign(assignment); + + expect(result.success).toBe(true); + expect(result.data).toEqual([assignment]); + + expect(mockApiClient.post).toHaveBeenCalledWith( + TASK_ENDPOINTS.ASSIGN_TASKS, + expect.objectContaining({ + taskAssignments: expect.arrayContaining([ + expect.objectContaining({ + taskId: assignment.taskId, + userId: assignment.userId + }) + ]) + }), + expect.any(Object) + ); + }); + + it('should assign multiple tasks successfully', async () => { + const assignments = [ + { taskId: TASK_TEST_CONSTANTS.TASK_ID, userId: TASK_TEST_CONSTANTS.USER_ID }, + { taskId: TASK_TEST_CONSTANTS.TASK_ID_2, userId: TASK_TEST_CONSTANTS.USER_ID_2 } + ] as TaskAssignmentOptions[]; + + const mockResponse = { + value: [] + }; + + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await taskService.assign(assignments); + + expect(result.success).toBe(true); + expect(result.data).toEqual(assignments); + + expect(mockApiClient.post).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + taskAssignments: expect.arrayContaining([ + expect.objectContaining({ taskId: TASK_TEST_CONSTANTS.TASK_ID, userId: TASK_TEST_CONSTANTS.USER_ID }), + expect.objectContaining({ taskId: TASK_TEST_CONSTANTS.TASK_ID_2, userId: TASK_TEST_CONSTANTS.USER_ID_2 }) + ]) + }), + expect.any(Object) + ); + }); + + it('should support assignment with email', async () => { + const assignment = { + taskId: TASK_TEST_CONSTANTS.TASK_ID, + userNameOrEmail: TASK_TEST_CONSTANTS.USER_EMAIL + } as TaskAssignmentOptions; + + const mockResponse = { + value: [] + }; + + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await taskService.assign(assignment); + + expect(result.success).toBe(true); + expect(result.data).toEqual([assignment]); + + expect(mockApiClient.post).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + taskAssignments: expect.arrayContaining([ + expect.objectContaining({ + taskId: TASK_TEST_CONSTANTS.TASK_ID, + userNameOrEmail: TASK_TEST_CONSTANTS.USER_EMAIL + }) + ]) + }), + expect.any(Object) + ); + }); + }); + + describe('reassign', () => { + it('should reassign a single task successfully', async () => { + const assignment = { + taskId: TASK_TEST_CONSTANTS.TASK_ID, + userId: TASK_TEST_CONSTANTS.USER_ID + } as TaskAssignmentOptions; + + const mockResponse = { + value: [] + }; + + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await taskService.reassign(assignment); + + expect(result.success).toBe(true); + expect(result.data).toEqual([assignment]); + + expect(mockApiClient.post).toHaveBeenCalledWith( + TASK_ENDPOINTS.REASSIGN_TASKS, + expect.objectContaining({ + taskAssignments: expect.arrayContaining([ + expect.objectContaining({ + taskId: assignment.taskId, + userId: assignment.userId + }) + ]) + }), + expect.any(Object) + ); + }); + + it('should reassign multiple tasks successfully', async () => { + const assignments = [ + { taskId: TASK_TEST_CONSTANTS.TASK_ID, userId: TASK_TEST_CONSTANTS.USER_ID }, + { taskId: TASK_TEST_CONSTANTS.TASK_ID_2, userId: TASK_TEST_CONSTANTS.USER_ID_2 } + ] as TaskAssignmentOptions[]; + + const mockResponse = { + value: [] + }; + + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await taskService.reassign(assignments); + + // Verify complete OperationResponse structure + expect(result.success).toBe(true); + expect(result.data).toEqual(assignments); + + // Verify API call + expect(mockApiClient.post).toHaveBeenCalledWith( + TASK_ENDPOINTS.REASSIGN_TASKS, + expect.objectContaining({ + taskAssignments: expect.arrayContaining([ + expect.objectContaining({ taskId: TASK_TEST_CONSTANTS.TASK_ID, userId: TASK_TEST_CONSTANTS.USER_ID }), + expect.objectContaining({ taskId: TASK_TEST_CONSTANTS.TASK_ID_2, userId: TASK_TEST_CONSTANTS.USER_ID_2 }) + ]) + }), + expect.any(Object) + ); + }); + + it('should reassign task with email address', async () => { + const assignment = { + taskId: TASK_TEST_CONSTANTS.TASK_ID, + userNameOrEmail: TASK_TEST_CONSTANTS.USER_EMAIL + } as TaskAssignmentOptions; + + const mockResponse = { + value: [] + }; + + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await taskService.reassign(assignment); + + expect(result.success).toBe(true); + expect(result.data).toEqual([assignment]); + + // Verify email is passed to API + expect(mockApiClient.post).toHaveBeenCalledWith( + TASK_ENDPOINTS.REASSIGN_TASKS, + expect.objectContaining({ + taskAssignments: expect.arrayContaining([ + expect.objectContaining({ + taskId: TASK_TEST_CONSTANTS.TASK_ID, + userNameOrEmail: TASK_TEST_CONSTANTS.USER_EMAIL + }) + ]) + }), + expect.any(Object) + ); + }); + }); + + describe('unassign', () => { + it('should unassign a single task successfully', async () => { + const taskId = TASK_TEST_CONSTANTS.TASK_ID; + + const mockResponse = { + value: [] + }; + + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await taskService.unassign(taskId); + + expect(result.success).toBe(true); + expect(result.data).toEqual([{ taskId: taskId }]); + + expect(mockApiClient.post).toHaveBeenCalledWith( + TASK_ENDPOINTS.UNASSIGN_TASKS, + expect.objectContaining({ + taskIds: [taskId] + }), + expect.any(Object) + ); + }); + + it('should unassign multiple tasks successfully', async () => { + const taskIds = [TASK_TEST_CONSTANTS.TASK_ID, TASK_TEST_CONSTANTS.TASK_ID_2, TASK_TEST_CONSTANTS.TASK_ID_3]; + + const mockResponse = { + value: [] + }; + + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await taskService.unassign(taskIds); + + expect(result.success).toBe(true); + expect(result.data).toEqual(taskIds.map(taskId => ({ taskId }))); + + expect(mockApiClient.post).toHaveBeenCalledWith( + TASK_ENDPOINTS.UNASSIGN_TASKS, + expect.objectContaining({ + taskIds + }), + expect.any(Object) + ); + }); + + it('should handle unassignment failure for invalid task ID', async () => { + const invalidTaskId = 9999; + + const mockErrorResponse = { + value: [{ + taskId: invalidTaskId, + userId: null, + errorCode: 1002, + errorMessage: 'Action does not exist.', + userNameOrEmail: null + }] + }; + + mockApiClient.post.mockResolvedValue(mockErrorResponse); + + const result = await taskService.unassign(invalidTaskId); + + expect(result.success).toBe(false); + expect(result.data).toEqual(mockErrorResponse.value); + expect(result.data[0]).toHaveProperty('taskId', invalidTaskId); + expect(result.data[0]).toHaveProperty('errorCode', 1002); + expect(result.data[0]).toHaveProperty('errorMessage', 'Action does not exist.'); + }); + }); + + describe('complete', () => { + it('should complete a generic task successfully', async () => { + const completionOptions = { + type: TaskType.External, + taskId: TASK_TEST_CONSTANTS.TASK_ID + } as TaskCompletionOptions; + + const folderId = TEST_CONSTANTS.FOLDER_ID; + + mockApiClient.post.mockResolvedValue(undefined); + + const result = await taskService.complete(completionOptions, folderId); + + expect(result.success).toBe(true); + expect(result.data).toEqual(completionOptions); + expect(mockApiClient.post).toHaveBeenCalledWith( + TASK_ENDPOINTS.COMPLETE_GENERIC_TASK, + completionOptions, + expect.objectContaining({ + headers: expect.any(Object) + }) + ); + }); + + it('should complete a form task successfully', async () => { + const completionOptions = { + type: TaskType.Form, + taskId: TASK_TEST_CONSTANTS.TASK_ID, + data: TASK_TEST_CONSTANTS.FORM_DATA, + action: TASK_TEST_CONSTANTS.ACTION_SUBMIT + } as TaskCompletionOptions; + + const folderId = TEST_CONSTANTS.FOLDER_ID; + + mockApiClient.post.mockResolvedValue(undefined); + + const result = await taskService.complete(completionOptions, folderId); + + expect(result.success).toBe(true); + expect(result.data).toEqual(completionOptions); + + expect(mockApiClient.post).toHaveBeenCalledWith( + TASK_ENDPOINTS.COMPLETE_FORM_TASK, + completionOptions, + expect.any(Object) + ); + }); + + it('should complete an app task successfully', async () => { + const completionOptions = { + type: TaskType.App, + taskId: TASK_TEST_CONSTANTS.TASK_ID, + action: TASK_TEST_CONSTANTS.ACTION_APPROVE, + data: TASK_TEST_CONSTANTS.APP_TASK_DATA + } as TaskCompletionOptions; + + const folderId = TEST_CONSTANTS.FOLDER_ID; + + mockApiClient.post.mockResolvedValue(undefined); + + const result = await taskService.complete(completionOptions, folderId); + + expect(result.success).toBe(true); + expect(result.data).toEqual(completionOptions); + + expect(mockApiClient.post).toHaveBeenCalledWith( + TASK_ENDPOINTS.COMPLETE_APP_TASK, + completionOptions, + expect.any(Object) + ); + }); + + it('should include folderId in headers', async () => { + const completionOptions = { + type: TaskType.External, + taskId: TASK_TEST_CONSTANTS.TASK_ID + } as TaskCompletionOptions; + + const folderId = TEST_CONSTANTS.FOLDER_ID; + + mockApiClient.post.mockResolvedValue(undefined); + + await taskService.complete(completionOptions, folderId); + + expect(mockApiClient.post).toHaveBeenCalledWith( + expect.any(String), + expect.any(Object), + expect.objectContaining({ + headers: expect.objectContaining({ + [FOLDER_ID]: folderId.toString() + }) + }) + ); + }); + }); + + describe('getById', () => { + it('should get a task by ID successfully', async () => { + const taskId = TASK_TEST_CONSTANTS.TASK_ID; + const mockResponse = createMockTaskGetResponse({ + id: taskId, + title: TASK_TEST_CONSTANTS.TASK_TITLE + }); + + mockApiClient.get.mockResolvedValue(mockResponse); + + const result = await taskService.getById(taskId); + + expect(result).toBeDefined(); + expect(result.id).toBe(taskId); + expect(result.title).toBe(TASK_TEST_CONSTANTS.TASK_TITLE); + expect(mockApiClient.get).toHaveBeenCalledWith( + expect.stringContaining(taskId.toString()), + expect.any(Object) + ); + }); + + it('should include folderId in headers when provided', async () => { + const taskId = TASK_TEST_CONSTANTS.TASK_ID; + const folderId = TEST_CONSTANTS.FOLDER_ID; + const mockResponse = createMockTaskGetResponse({ + id: taskId, + folderId: folderId + }); + + mockApiClient.get.mockResolvedValue(mockResponse); + + await taskService.getById(taskId, {}, folderId); + + expect(mockApiClient.get).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: expect.objectContaining({ + [FOLDER_ID]: folderId.toString() + }) + }) + ); + }); + + it('should handle form tasks by calling getFormTaskById with provided folderId', async () => { + const taskId = TASK_TEST_CONSTANTS.TASK_ID; + const folderId = TEST_CONSTANTS.FOLDER_ID; + + const mockTaskResponse = createMockTaskGetResponse({ + id: taskId, + title: TASK_TEST_CONSTANTS.TASK_TITLE_FORM, + type: TaskType.Form, + folderId: folderId + }); + + const mockFormTaskResponse = createMockTaskGetResponse({ + id: taskId, + title: TASK_TEST_CONSTANTS.TASK_TITLE_FORM, + type: TaskType.Form, + folderId: folderId, + formLayout: { /* form-specific data */ }, + actionLabel: TASK_TEST_CONSTANTS.ACTION_SUBMIT + }); + + mockApiClient.get + .mockResolvedValueOnce(mockTaskResponse) + .mockResolvedValueOnce(mockFormTaskResponse); + + await taskService.getById(taskId, {}, folderId); + + expect(mockApiClient.get).toHaveBeenCalledTimes(2); + expect(mockApiClient.get).toHaveBeenNthCalledWith( + 2, + TASK_ENDPOINTS.GET_TASK_FORM_BY_ID, + expect.any(Object) + ); + }); + + it('should handle form tasks without folderId by using task folderId', async () => { + const taskId = TASK_TEST_CONSTANTS.TASK_ID; + const taskFolderId = TEST_CONSTANTS.FOLDER_ID; + + const mockTaskResponse = createMockTaskGetResponse({ + id: taskId, + title: TASK_TEST_CONSTANTS.TASK_TITLE_FORM, + type: TaskType.Form, + folderId: taskFolderId + }); + + const mockFormTaskResponse = createMockTaskGetResponse({ + id: taskId, + title: TASK_TEST_CONSTANTS.TASK_TITLE_FORM, + type: TaskType.Form, + folderId: taskFolderId, + formLayout: { /* form-specific data */ }, + actionLabel: TASK_TEST_CONSTANTS.ACTION_SUBMIT + }); + + mockApiClient.get + .mockResolvedValueOnce(mockTaskResponse) + .mockResolvedValueOnce(mockFormTaskResponse); + + // Call without providing folderId parameter + await taskService.getById(taskId); + + expect(mockApiClient.get).toHaveBeenCalledTimes(2); + expect(mockApiClient.get).toHaveBeenNthCalledWith( + 2, + TASK_ENDPOINTS.GET_TASK_FORM_BY_ID, + expect.any(Object) + ); + }); + + it('should merge custom expand with default expand parameters', async () => { + const taskId = TASK_TEST_CONSTANTS.TASK_ID; + const mockResponse = createMockTaskGetResponse({ + id: taskId, + title: TASK_TEST_CONSTANTS.TASK_TITLE + }); + + mockApiClient.get.mockResolvedValue(mockResponse); + + // Test with custom expand parameter + await taskService.getById(taskId, { expand: 'CustomField' }); + + expect(mockApiClient.get).toHaveBeenCalledWith( + expect.stringContaining(taskId.toString()), + expect.objectContaining({ + params: expect.objectContaining({ + expand: `${DEFAULT_TASK_EXPAND},CustomField` + }) + }) + ); + }); + }); + + describe('getAll', () => { + beforeEach(() => { + // Reset the mock before each test + vi.mocked(PaginationHelpers.getAll).mockReset(); + }); + + it('should return all tasks without pagination', async () => { + // Mock the pagination helper to return our test data + const mockTasks = createMockTasks(3); + const mockResponse = { + items: mockTasks, + totalCount: 3 + }; + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const result = await taskService.getAll(); + + // Verify PaginationHelpers.getAll was called with correct parameters + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.any(Function), + transformFn: expect.any(Function), + pagination: expect.any(Object) + }), + undefined + ); + + expect(result).toEqual(mockResponse); + }); + + it('should return paginated tasks when pagination options provided', async () => { + // Mock the pagination helper to return our test data + const mockTasks = createMockTasks(10); + const mockResponse = { + items: mockTasks, + totalCount: 100, + hasNextPage: true, + nextCursor: TASK_TEST_CONSTANTS.CURSOR_NEXT, + previousCursor: null, + currentPage: 1, + totalPages: 10 + }; + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const options = { + pageSize: TEST_CONSTANTS.PAGE_SIZE, + jumpToPage: 1 + } as TaskGetAllOptions; + + const result = await taskService.getAll(options); + + // Verify PaginationHelpers.getAll was called with correct parameters + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.any(Function), + transformFn: expect.any(Function), + pagination: expect.any(Object) + }), + expect.objectContaining({ + pageSize: TEST_CONSTANTS.PAGE_SIZE, + jumpToPage: 1 + }) + ); + + expect(result).toEqual(mockResponse); + }); + + it('should handle filtering options', async () => { + // Mock the pagination helper to return our test data + const mockTasks = createMockTasks(2); + const mockResponse = { + items: mockTasks, + totalCount: 2 + }; + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const options = { + filter: "status eq 'Pending'" + } as TaskGetAllOptions; + + await taskService.getAll(options); + + // Verify PaginationHelpers.getAll was called with correct parameters + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.any(Function), + transformFn: expect.any(Function), + pagination: expect.any(Object) + }), + expect.objectContaining({ + filter: "status eq 'Pending'" + }) + ); + }); + + it('should call processParametersFn with folderId when provided', async () => { + const mockTasks = createMockTasks(1); + const mockResponse = { + items: mockTasks, + totalCount: 1 + }; + + // Mock PaginationHelpers.getAll and capture the processParametersFn + let capturedProcessParametersFn: ((options: any, folderId?: number) => any) | undefined; + vi.mocked(PaginationHelpers.getAll).mockImplementation(async (config: any) => { + capturedProcessParametersFn = config.processParametersFn; + return mockResponse; + }); + + await taskService.getAll({ folderId: TEST_CONSTANTS.FOLDER_ID }); + + // Verify the process parameters function was captured + expect(capturedProcessParametersFn).toBeDefined(); + + // Test processParametersFn with folderId and no existing filter + const optionsWithoutFilter = { select: 'id,title' }; + const processedWithoutFilter = capturedProcessParametersFn!(optionsWithoutFilter, TEST_CONSTANTS.FOLDER_ID); + expect(processedWithoutFilter).toHaveProperty('filter', `organizationUnitId eq ${TEST_CONSTANTS.FOLDER_ID}`); + expect(processedWithoutFilter).toHaveProperty('expand'); + + // Test processParametersFn with folderId and existing filter + const optionsWithFilter = { filter: 'status eq "Pending"' }; + const processedWithFilter = capturedProcessParametersFn!(optionsWithFilter, TEST_CONSTANTS.FOLDER_ID); + expect(processedWithFilter.filter).toBe(`status eq "Pending" and organizationUnitId eq ${TEST_CONSTANTS.FOLDER_ID}`); + + // Test processParametersFn without folderId + const optionsNoFolder = { select: 'id' }; + const processedNoFolder = capturedProcessParametersFn!(optionsNoFolder); + expect(processedNoFolder.filter).toBeUndefined(); + }); + }); + + describe('getUsers', () => { + beforeEach(() => { + // Reset the mock before each test + vi.mocked(PaginationHelpers.getAll).mockReset(); + }); + + it('should return all users without pagination', async () => { + // Mock the pagination helper to return our test data + const folderId = TEST_CONSTANTS.FOLDER_ID; + const mockUsers = createMockUsers(3); + const mockResponse = { + items: mockUsers, + totalCount: 3 + }; + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const result = await taskService.getUsers(folderId); + + // Verify PaginationHelpers.getAll was called with correct parameters + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.any(Function), + transformFn: expect.any(Function), + pagination: expect.any(Object) + }), + expect.objectContaining({ + folderId: folderId + }) + ); + + expect(result).toEqual(mockResponse); + }); + + it('should return paginated users when pagination options provided', async () => { + // Mock the pagination helper to return our test data + const folderId = TEST_CONSTANTS.FOLDER_ID; + const mockUsers = createMockUsers(10); + const mockResponse = { + items: mockUsers, + totalCount: 50, + hasNextPage: true, + nextCursor: TASK_TEST_CONSTANTS.CURSOR_NEXT, + previousCursor: null, + currentPage: 1, + totalPages: 5 + }; + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const options = { + pageSize: TEST_CONSTANTS.PAGE_SIZE + } as TaskGetUsersOptions; + + const result = await taskService.getUsers(folderId, options); + + // Verify PaginationHelpers.getAll was called with correct parameters + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.any(Function), + transformFn: expect.any(Function), + pagination: expect.any(Object) + }), + expect.objectContaining({ + folderId: folderId, + pageSize: TEST_CONSTANTS.PAGE_SIZE + }) + ); + + expect(result).toEqual(mockResponse); + }); + + it('should handle filtering options', async () => { + // Mock the pagination helper to return our test data + const folderId = TEST_CONSTANTS.FOLDER_ID; + const mockUsers = createMockUsers(1); + const mockResponse = { + items: mockUsers, + totalCount: 1 + }; + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const options = { + filter: "name eq 'abc'" + } as TaskGetUsersOptions; + + await taskService.getUsers(folderId, options); + + // Verify PaginationHelpers.getAll was called with correct parameters + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.any(Function), + transformFn: expect.any(Function), + pagination: expect.any(Object) + }), + expect.objectContaining({ + folderId: folderId, + filter: "name eq 'abc'" + }) + ); + }); + + it('should handle API errors', async () => { + const error = createMockError(TEST_CONSTANTS.ERROR_MESSAGE); + vi.mocked(PaginationHelpers.getAll).mockRejectedValue(error); + + await expect(taskService.getUsers(TEST_CONSTANTS.FOLDER_ID)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); +}); diff --git a/tests/unit/services/tasks.test.ts b/tests/unit/services/tasks.test.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/utils/constants/index.ts b/tests/utils/constants/index.ts index 8b3be0eb..34981c15 100644 --- a/tests/utils/constants/index.ts +++ b/tests/utils/constants/index.ts @@ -3,4 +3,6 @@ */ export * from './common'; -export * from './maestro'; \ No newline at end of file +export * from './maestro'; +export * from './tasks'; + diff --git a/tests/utils/constants/tasks.ts b/tests/utils/constants/tasks.ts new file mode 100644 index 00000000..4028e11e --- /dev/null +++ b/tests/utils/constants/tasks.ts @@ -0,0 +1,58 @@ +/** + * Task service test constants + * Task-specific constants only + */ + +export const TASK_TEST_CONSTANTS = { + // Task IDs + TASK_ID: 123, + TASK_ID_2: 456, + TASK_ID_3: 789, + + // User IDs + USER_ID: 456, + USER_ID_2: 101, + + // Task Metadata + TASK_TITLE: 'Test Task', + TASK_TITLE_COMPLEX: 'Complex Task', + TASK_TITLE_FORM: 'Form Task', + TASK_KEY: 'TASK-123', + TASK_KEY_PREFIX: 'TASK-', + + // User Information + USER_NAME: 'User', + USER_USERNAME_PREFIX: 'user', + USER_DISPLAY_NAME_PREFIX: 'User ', + USER_EMAIL: 'user@example.com', + + // Timestamps + CREATION_TIME: '2025-01-15T10:00:00Z', + + // Pagination + CURSOR_NEXT: 'next-cursor-string', + + // Task Actions + ACTION_SUBMIT: 'submit', + ACTION_APPROVE: 'approve', + + // Task Form Data + FORM_DATA: { + fieldName: 'John Doe', + fieldEmail: 'john@example.com', + fieldNotes: 'Completed the form' + }, + + // Task Custom Data + CUSTOM_DATA: { + customField: 'customValue', + nested: { key: 'value' }, + array: [1, 2, 3] + }, + + // App Task Completion Data + APP_TASK_DATA: { + Content: null, + Comment: null + }, +} as const; diff --git a/tests/utils/mocks/core.ts b/tests/utils/mocks/core.ts index c27753a6..f2459e02 100644 --- a/tests/utils/mocks/core.ts +++ b/tests/utils/mocks/core.ts @@ -4,6 +4,38 @@ import { vi } from 'vitest'; import { TEST_CONSTANTS } from '../constants/common'; +/** + * Ready-to-use mock for transform utilities + * Import and spread this in your vi.mock() call + * + * @example + * vi.mock('../../../src/utils/transform', () => mockTransformUtils); + */ +export const mockTransformUtils = { + pascalToCamelCaseKeys: vi.fn((obj) => obj), + camelToPascalCaseKeys: vi.fn((obj) => obj), + transformData: vi.fn((data) => data), + applyDataTransforms: vi.fn((data) => data), + addPrefixToKeys: vi.fn((obj) => obj), +}; + +/** + * Ready-to-use mock for PaginationHelpers + * Import and spread this in your vi.mock() call + * + * @example + * vi.mock('../../../src/utils/pagination/helpers', () => mockPaginationHelpers); + */ +export const mockPaginationHelpers = { + PaginationHelpers: { + getAll: vi.fn(), + hasPaginationParameters: vi.fn((options = {}) => { + const { cursor, pageSize, jumpToPage } = options; + return cursor !== undefined || pageSize !== undefined || jumpToPage !== undefined; + }) + } +}; + /** * Generic factory for creating mock base response objects * Use this as a building block for service-specific responses @@ -16,6 +48,8 @@ import { TEST_CONSTANTS } from '../constants/common'; * ```typescript * const mockResponse = createMockBaseResponse( * { id: 'test', name: 'Test' }, + * const mockTask = createMockBaseResponse( + * { id: 123, title: 'Test', folderId: 456 }, * { customField: 'value' } * ); * ``` @@ -59,25 +93,25 @@ export const createMockOperationResponse = (data: T): { success: boolean; dat }); /** - * Pagination helpers mock - Used across all services that need pagination - * This provides a consistent mock for PaginationHelpers across all test files + * Generic factory for creating a collection of mock responses + * Useful for testing list/getAll endpoints * - * Usage in test files: - * ```typescript - * // Use vi.hoisted to ensure mockPaginationHelpers is available during hoisting - * const mocks = vi.hoisted(() => { - * return import('../../../utils/mocks/core'); - * }); + * @param count - Number of mock items to create + * @param factory - Function that creates a single mock item given an index + * @returns Array of mock items * - * vi.mock('../../../../src/utils/pagination/helpers', async () => (await mocks).mockPaginationHelpers); + * @example + * ```typescript + * const mockTasks = createMockCollection(3, (i) => ({ + * id: i + 1, + * title: `Task ${i + 1}`, + * status: 'Active' + * })); * ``` */ -export const mockPaginationHelpers = { - PaginationHelpers: { - getAll: vi.fn(), - hasPaginationParameters: vi.fn((options = {}) => { - const { cursor, pageSize, jumpToPage } = options; - return cursor !== undefined || pageSize !== undefined || jumpToPage !== undefined; - }) - } +export const createMockCollection = ( + count: number, + factory: (index: number) => T +): T[] => { + return Array.from({ length: count }, (_, i) => factory(i)); }; diff --git a/tests/utils/mocks/index.ts b/tests/utils/mocks/index.ts index 22cea9b7..5bce619a 100644 --- a/tests/utils/mocks/index.ts +++ b/tests/utils/mocks/index.ts @@ -8,6 +8,7 @@ export * from './core'; // Service-specific mock utilities export * from './maestro'; +export * from './tasks'; // Re-export constants for convenience export * from '../constants'; \ No newline at end of file diff --git a/tests/utils/mocks/tasks.ts b/tests/utils/mocks/tasks.ts new file mode 100644 index 00000000..c4d18109 --- /dev/null +++ b/tests/utils/mocks/tasks.ts @@ -0,0 +1,148 @@ +/** + * Tasks service mock utilities - Tasks-specific mocks only + * Uses generic utilities from core.ts for base functionality + */ + +import { TaskType, TaskPriority, TaskStatus, RawTaskGetResponse } from '../../../src/models/action-center/tasks.types'; +import { createMockBaseResponse, createMockCollection } from './core'; +import { TASK_TEST_CONSTANTS } from '../constants/tasks'; +import { TEST_CONSTANTS } from '../constants/common'; + +// Task-Specific Mock Factories + +/** + * Creates a mock Task response for create/update operations + * @param overrides - Optional overrides for specific fields + * @returns Mock Task response object + */ +export const createMockTaskResponse = (overrides: Partial = {}) => { + return createMockBaseResponse({ + id: TASK_TEST_CONSTANTS.TASK_ID, + title: TASK_TEST_CONSTANTS.TASK_TITLE, + type: TaskType.External, + priority: TaskPriority.Medium, + status: TaskStatus.Unassigned, + organizationUnitId: TEST_CONSTANTS.FOLDER_ID, + key: TASK_TEST_CONSTANTS.TASK_KEY, + isDeleted: false, + creationTime: TASK_TEST_CONSTANTS.CREATION_TIME, + action: null, + externalTag: null, + lastAssignedTime: null, + completionTime: null, + parentOperationId: null, + deleterUserId: null, + deletionTime: null, + lastModificationTime: null, + waitJobState: null, + assignedToUser: null, + taskSlaDetails: null, + completedByUser: null, + taskAssignees: null, + processingTime: null, + data: null, + }, overrides); +}; + +/** + * Creates a mock Task GET response + * @param overrides - Optional overrides for specific fields + * @returns Mock Task GET response object + */ +export const createMockTaskGetResponse = (overrides: Partial = {}) => { + return createMockBaseResponse({ + id: TASK_TEST_CONSTANTS.TASK_ID, + title: TASK_TEST_CONSTANTS.TASK_TITLE, + type: TaskType.External, + priority: TaskPriority.Medium, + status: TaskStatus.Unassigned, + organizationUnitId: TEST_CONSTANTS.FOLDER_ID, + key: TASK_TEST_CONSTANTS.TASK_KEY, + isDeleted: false, + creationTime: TASK_TEST_CONSTANTS.CREATION_TIME, + action: null, + externalTag: null, + lastAssignedTime: null, + completionTime: null, + parentOperationId: null, + deleterUserId: null, + deletionTime: null, + lastModificationTime: null, + isCompleted: false, + encrypted: false, + bulkFormLayoutId: null, + formLayoutId: null, + taskSlaDetail: null, + taskAssigneeName: null, + lastModifierUserId: null, + assignedToUser: null, + }, overrides); +}; + +/** + * Creates a basic task for model tests with all required fields + * @param overrides - Optional overrides for specific fields + * @returns Basic task response object cast as RawTaskGetResponse + */ +export const createBasicTask = (overrides: Partial = {}): RawTaskGetResponse => { + return createMockBaseResponse({ + id: TASK_TEST_CONSTANTS.TASK_ID, + title: TASK_TEST_CONSTANTS.TASK_TITLE, + type: TaskType.External, + priority: TaskPriority.Medium, + status: TaskStatus.Unassigned, + folderId: TEST_CONSTANTS.FOLDER_ID, + key: TASK_TEST_CONSTANTS.TASK_KEY, + isDeleted: false, + createdTime: TASK_TEST_CONSTANTS.CREATION_TIME, + action: null, + externalTag: null, + lastAssignedTime: null, + completedTime: null, + parentOperationId: null, + deleterUserId: null, + deletedTime: null, + lastModifiedTime: null, + isCompleted: false, + encrypted: false, + bulkFormLayoutId: null, + formLayoutId: null, + taskSlaDetail: null, + taskAssigneeName: null, + lastModifierUserId: null, + assignedToUser: null, + }, overrides) as RawTaskGetResponse; +}; + +/** + * Creates a collection of mock tasks + * @param count - Number of tasks to create + * @returns Array of mock tasks + */ +export const createMockTasks = (count: number) => { + return createMockCollection(count, (i) => + createMockTaskGetResponse({ + id: i + 1, + title: `Task ${i + 1}`, + key: `${TASK_TEST_CONSTANTS.TASK_KEY_PREFIX}${i + 1}`, + status: TaskStatus.Pending, + }) + ); +}; + +/** + * Creates a collection of mock users + * @param count - Number of users to create + * @returns Array of mock users + */ +export const createMockUsers = (count: number) => { + return createMockCollection(count, (i) => ({ + id: i + 1, + name: TASK_TEST_CONSTANTS.USER_NAME, + surname: `${i + 1}`, + userName: `${TASK_TEST_CONSTANTS.USER_USERNAME_PREFIX}${i + 1}`, + emailAddress: `${TASK_TEST_CONSTANTS.USER_USERNAME_PREFIX}${i + 1}@example.com`, + displayName: `${TASK_TEST_CONSTANTS.USER_DISPLAY_NAME_PREFIX}${i + 1}`, + })); +}; + From 2a333925a51256123115c732ba6add5183edfc85 Mon Sep 17 00:00:00 2001 From: swati354 <103816801+swati354@users.noreply.github.com> Date: Fri, 17 Oct 2025 15:02:38 +0530 Subject: [PATCH 4/7] Add unit tests for data fabric (#106) Add test cases for Data Fabric --- .../unit/models/data-fabric/entities.test.ts | 393 ++++++++++ tests/unit/models/data-fabric/index.ts | 6 + .../services/data-fabric/entities.test.ts | 686 ++++++++++++++++++ tests/unit/services/data-fabric/index.ts | 6 + tests/utils/constants/common.ts | 6 + tests/utils/constants/entities.ts | 88 +++ tests/utils/constants/index.ts | 1 + tests/utils/mocks/entities.ts | 651 +++++++++++++++++ tests/utils/mocks/index.ts | 1 + 9 files changed, 1838 insertions(+) create mode 100644 tests/unit/models/data-fabric/entities.test.ts create mode 100644 tests/unit/models/data-fabric/index.ts create mode 100644 tests/unit/services/data-fabric/entities.test.ts create mode 100644 tests/unit/services/data-fabric/index.ts create mode 100644 tests/utils/constants/entities.ts create mode 100644 tests/utils/mocks/entities.ts diff --git a/tests/unit/models/data-fabric/entities.test.ts b/tests/unit/models/data-fabric/entities.test.ts new file mode 100644 index 00000000..b54405fc --- /dev/null +++ b/tests/unit/models/data-fabric/entities.test.ts @@ -0,0 +1,393 @@ +// ===== IMPORTS ===== +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { createEntityWithMethods } from '../../../../src/models/data-fabric/entities.models'; +import type { EntityServiceModel } from '../../../../src/models/data-fabric/entities.models'; +import { createBasicEntity, createMockEntityRecords, createMockInsertResponse, createMockUpdateResponse, createMockDeleteResponse } from '../../../utils/mocks/entities'; +import { ENTITY_TEST_CONSTANTS } from '../../../utils/constants/entities'; +import { TEST_CONSTANTS } from '../../../utils/constants/common'; + +// ===== TEST SUITE ===== +describe('Entity Models', () => { + let mockService: EntityServiceModel; + + beforeEach(() => { + // Create a mock service + mockService = { + getAll: vi.fn(), + getById: vi.fn(), + getRecordsById: vi.fn(), + insertById: vi.fn(), + updateById: vi.fn(), + deleteById: vi.fn(), + } as any; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('bound methods on entity', () => { + describe('entity.insert()', () => { + it('should call entity.insert with entity id and data', async () => { + const entityData = createBasicEntity(); + const entity = createEntityWithMethods(entityData, mockService); + + const testData = [ + ENTITY_TEST_CONSTANTS.TEST_RECORD_DATA, + ENTITY_TEST_CONSTANTS.TEST_RECORD_DATA_2 + ]; + const mockResponse = createMockInsertResponse(testData); + mockService.insertById = vi.fn().mockResolvedValue(mockResponse); + + const result = await entity.insert(testData); + + expect(mockService.insertById).toHaveBeenCalledWith( + ENTITY_TEST_CONSTANTS.ENTITY_ID, + testData, + undefined + ); + expect(result).toEqual(mockResponse); + expect(result.successRecords).toHaveLength(2); + expect(result.failureRecords).toHaveLength(0); + }); + + it('should call entity.insert with options', async () => { + const entityData = createBasicEntity(); + const entity = createEntityWithMethods(entityData, mockService); + + const testData = [ENTITY_TEST_CONSTANTS.TEST_RECORD_DATA]; + const options = { + expansionLevel: ENTITY_TEST_CONSTANTS.EXPANSION_LEVEL, + failOnFirst: ENTITY_TEST_CONSTANTS.FAIL_ON_FIRST + }; + const mockResponse = createMockInsertResponse(testData, { + expansionLevel: ENTITY_TEST_CONSTANTS.EXPANSION_LEVEL + }); + mockService.insertById = vi.fn().mockResolvedValue(mockResponse); + + const result = await entity.insert(testData, options); + + expect(mockService.insertById).toHaveBeenCalledWith( + ENTITY_TEST_CONSTANTS.ENTITY_ID, + testData, + options + ); + expect(result).toEqual(mockResponse); + }); + + it('should throw error if entity id is undefined', async () => { + const entityData = createBasicEntity({ id: undefined as any }); + const entity = createEntityWithMethods(entityData, mockService); + + await expect(entity.insert([ENTITY_TEST_CONSTANTS.TEST_RECORD_DATA])).rejects.toThrow(ENTITY_TEST_CONSTANTS.ERROR_MESSAGE_ENTITY_ID_UNDEFINED); + }); + + it('should handle partial failures in insert', async () => { + const entityData = createBasicEntity(); + const entity = createEntityWithMethods(entityData, mockService); + + const testData = [ + ENTITY_TEST_CONSTANTS.TEST_RECORD_DATA, + { name: ENTITY_TEST_CONSTANTS.TEST_INVALID_RECORD_NAME, age: null } // Missing required field + ]; + const mockResponse = createMockInsertResponse(testData, { successCount: 1 }); + mockService.insertById = vi.fn().mockResolvedValue(mockResponse); + + const result = await entity.insert(testData); + + expect(result.successRecords).toHaveLength(1); + expect(result.failureRecords).toHaveLength(1); + + // Validate successful record data + expect(result.successRecords[0]).toHaveProperty('id'); + expect(result.successRecords[0].name).toBe(testData[0].name); + expect(result.successRecords[0].age).toBe(testData[0].age); + + // Validate failure record structure + expect(result.failureRecords[0]).toHaveProperty('error'); + expect(result.failureRecords[0]).toHaveProperty('record'); + expect(result.failureRecords[0].record).toEqual(testData[1]); + expect(typeof result.failureRecords[0].error).toBe('string'); + }); + }); + + describe('entity.update()', () => { + it('should call entity.update with entity id and data', async () => { + const entityData = createBasicEntity(); + const entity = createEntityWithMethods(entityData, mockService); + + const testData = [ + { id: ENTITY_TEST_CONSTANTS.RECORD_ID, name: ENTITY_TEST_CONSTANTS.TEST_JOHN_UPDATED_NAME, age: ENTITY_TEST_CONSTANTS.TEST_JOHN_UPDATED_AGE }, + { id: ENTITY_TEST_CONSTANTS.RECORD_ID_2, name: ENTITY_TEST_CONSTANTS.TEST_JANE_UPDATED_NAME, age: ENTITY_TEST_CONSTANTS.TEST_JANE_UPDATED_AGE } + ]; + const mockResponse = createMockUpdateResponse(testData); + mockService.updateById = vi.fn().mockResolvedValue(mockResponse); + + const result = await entity.update(testData); + + expect(mockService.updateById).toHaveBeenCalledWith( + ENTITY_TEST_CONSTANTS.ENTITY_ID, + testData, + undefined + ); + expect(result).toEqual(mockResponse); + expect(result.successRecords).toHaveLength(2); + expect(result.failureRecords).toHaveLength(0); + }); + + it('should call entity.update with options', async () => { + const entityData = createBasicEntity(); + const entity = createEntityWithMethods(entityData, mockService); + + const testData = [ + { id: ENTITY_TEST_CONSTANTS.RECORD_ID, name: ENTITY_TEST_CONSTANTS.TEST_JOHN_UPDATED_NAME, age: ENTITY_TEST_CONSTANTS.TEST_JOHN_UPDATED_AGE } + ]; + const options = { + expansionLevel: ENTITY_TEST_CONSTANTS.EXPANSION_LEVEL, + failOnFirst: ENTITY_TEST_CONSTANTS.FAIL_ON_FIRST + }; + const mockResponse = createMockUpdateResponse(testData, { + expansionLevel: ENTITY_TEST_CONSTANTS.EXPANSION_LEVEL + }); + mockService.updateById = vi.fn().mockResolvedValue(mockResponse); + + const result = await entity.update(testData, options); + + expect(mockService.updateById).toHaveBeenCalledWith( + ENTITY_TEST_CONSTANTS.ENTITY_ID, + testData, + options + ); + expect(result).toEqual(mockResponse); + + // Validate response structure and data + expect(result.successRecords).toHaveLength(1); + expect(result.failureRecords).toHaveLength(0); + expect(result.successRecords[0].id).toBe(testData[0].id); + expect(result.successRecords[0].name).toBe(testData[0].name); + expect(result.successRecords[0].age).toBe(testData[0].age); + + // Verify expansion level affected the data (reference fields should be objects) + if (result.successRecords[0].updatedBy) { + expect(typeof result.successRecords[0].updatedBy).toBe('object'); + expect(result.successRecords[0].updatedBy).toHaveProperty('id'); + } + }); + + it('should throw error if entity id is undefined', async () => { + const entityData = createBasicEntity({ id: undefined as any }); + const entity = createEntityWithMethods(entityData, mockService); + + await expect(entity.update([ + { id: ENTITY_TEST_CONSTANTS.RECORD_ID, name: ENTITY_TEST_CONSTANTS.TEST_UPDATED_NAME } + ])).rejects.toThrow(ENTITY_TEST_CONSTANTS.ERROR_MESSAGE_ENTITY_ID_UNDEFINED); + }); + + it('should handle partial failures in update', async () => { + const entityData = createBasicEntity(); + const entity = createEntityWithMethods(entityData, mockService); + + const testData = [ + { id: ENTITY_TEST_CONSTANTS.RECORD_ID, name: ENTITY_TEST_CONSTANTS.TEST_VALID_UPDATE_NAME }, + { id: ENTITY_TEST_CONSTANTS.TEST_INVALID_ID, name: ENTITY_TEST_CONSTANTS.TEST_INVALID_UPDATE_NAME } + ]; + const mockResponse = createMockUpdateResponse(testData, { successCount: 1 }); + mockService.updateById = vi.fn().mockResolvedValue(mockResponse); + + const result = await entity.update(testData); + + expect(result.successRecords).toHaveLength(1); + expect(result.failureRecords).toHaveLength(1); + + // Validate successful record data + expect(result.successRecords[0].id).toBe(testData[0].id); + expect(result.successRecords[0].name).toBe(testData[0].name); + + // Validate failure record structure + expect(result.failureRecords[0]).toHaveProperty('error'); + expect(result.failureRecords[0]).toHaveProperty('record'); + expect(result.failureRecords[0].record).toEqual(testData[1]); + expect(typeof result.failureRecords[0].error).toBe('string'); + }); + }); + + describe('entity.delete()', () => { + it('should call entity.delete with entity id and record ids', async () => { + const entityData = createBasicEntity(); + const entity = createEntityWithMethods(entityData, mockService); + + const recordIds = [ + ENTITY_TEST_CONSTANTS.RECORD_ID, + ENTITY_TEST_CONSTANTS.RECORD_ID_2 + ]; + const mockResponse = createMockDeleteResponse(recordIds); + mockService.deleteById = vi.fn().mockResolvedValue(mockResponse); + + const result = await entity.delete(recordIds); + + expect(mockService.deleteById).toHaveBeenCalledWith( + ENTITY_TEST_CONSTANTS.ENTITY_ID, + recordIds, + undefined + ); + expect(result).toEqual(mockResponse); + expect(result.successRecords).toHaveLength(2); + expect(result.failureRecords).toHaveLength(0); + }); + + it('should call entity.delete with options', async () => { + const entityData = createBasicEntity(); + const entity = createEntityWithMethods(entityData, mockService); + + const recordIds = [ENTITY_TEST_CONSTANTS.RECORD_ID]; + const options = { + failOnFirst: ENTITY_TEST_CONSTANTS.FAIL_ON_FIRST + }; + const mockResponse = createMockDeleteResponse(recordIds); + mockService.deleteById = vi.fn().mockResolvedValue(mockResponse); + + const result = await entity.delete(recordIds, options); + + expect(mockService.deleteById).toHaveBeenCalledWith( + ENTITY_TEST_CONSTANTS.ENTITY_ID, + recordIds, + options + ); + expect(result).toEqual(mockResponse); + + // Validate response structure and data + expect(result.successRecords).toHaveLength(1); + expect(result.failureRecords).toHaveLength(0); + expect(result.successRecords[0]).toHaveProperty('id'); + expect(result.successRecords[0].id).toBe(recordIds[0]); + }); + + it('should throw error if entity id is undefined', async () => { + const entityData = createBasicEntity({ id: undefined as any }); + const entity = createEntityWithMethods(entityData, mockService); + + await expect(entity.delete([ENTITY_TEST_CONSTANTS.RECORD_ID])).rejects.toThrow(ENTITY_TEST_CONSTANTS.ERROR_MESSAGE_ENTITY_ID_UNDEFINED); + }); + + it('should handle partial failures in delete', async () => { + const entityData = createBasicEntity(); + const entity = createEntityWithMethods(entityData, mockService); + + const recordIds = [ + ENTITY_TEST_CONSTANTS.RECORD_ID, + ENTITY_TEST_CONSTANTS.TEST_INVALID_ID + ]; + const mockResponse = createMockDeleteResponse(recordIds, { successCount: 1 }); + mockService.deleteById = vi.fn().mockResolvedValue(mockResponse); + + const result = await entity.delete(recordIds); + + expect(result.successRecords).toHaveLength(1); + expect(result.failureRecords).toHaveLength(1); + + // Validate successful deletion + expect(result.successRecords[0]).toHaveProperty('id'); + expect(result.successRecords[0].id).toBe(recordIds[0]); + + // Validate failure record structure + expect(result.failureRecords[0]).toHaveProperty('error'); + expect(result.failureRecords[0]).toHaveProperty('record'); + expect(result.failureRecords[0].record?.id).toBe(recordIds[1]); + expect(typeof result.failureRecords[0].error).toBe('string'); + }); + }); + + describe('entity.getRecords()', () => { + it('should call entity.getRecords without options', async () => { + const entityData = createBasicEntity(); + const entity = createEntityWithMethods(entityData, mockService); + + const mockRecords = createMockEntityRecords(5); + const mockResponse = { + items: mockRecords, + totalCount: 5 + }; + mockService.getRecordsById = vi.fn().mockResolvedValue(mockResponse); + + const result = await entity.getRecords(); + + expect(mockService.getRecordsById).toHaveBeenCalledWith( + ENTITY_TEST_CONSTANTS.ENTITY_ID, + undefined + ); + expect(result).toEqual(mockResponse); + expect(result.items).toHaveLength(5); + }); + + it('should call entity.getRecords with expansion level', async () => { + const entityData = createBasicEntity(); + const entity = createEntityWithMethods(entityData, mockService); + + const options = { + expansionLevel: ENTITY_TEST_CONSTANTS.EXPANSION_LEVEL + }; + const mockRecords = createMockEntityRecords(3, { + expansionLevel: ENTITY_TEST_CONSTANTS.EXPANSION_LEVEL + }); + const mockResponse = { + items: mockRecords, + totalCount: 3 + }; + mockService.getRecordsById = vi.fn().mockResolvedValue(mockResponse); + + const result = await entity.getRecords(options); + + expect(mockService.getRecordsById).toHaveBeenCalledWith( + ENTITY_TEST_CONSTANTS.ENTITY_ID, + options + ); + expect(result).toEqual(mockResponse); + + // Validate response structure and data + expect(result.items).toHaveLength(3); + expect(result.totalCount).toBe(3); + + // Verify expansion level affected the data (reference fields should be objects) + result.items.forEach(record => { + expect(record).toHaveProperty('id'); + if (record.recordOwner) { + expect(typeof record.recordOwner).toBe('object'); + expect(record.recordOwner).toHaveProperty('id'); + } + }); + }); + + it('should throw error if entity id is undefined', async () => { + const entityData = createBasicEntity({ id: undefined as any }); + const entity = createEntityWithMethods(entityData, mockService); + + await expect(entity.getRecords()).rejects.toThrow(ENTITY_TEST_CONSTANTS.ERROR_MESSAGE_ENTITY_ID_UNDEFINED); + }); + }); + }); + + describe('Entity data and methods are combined correctly', () => { + it('should preserve all entity properties', () => { + const entityData = createBasicEntity(); + const entity = createEntityWithMethods(entityData, mockService); + + expect(entity.id).toBe(ENTITY_TEST_CONSTANTS.ENTITY_ID); + expect(entity.name).toBe(ENTITY_TEST_CONSTANTS.ENTITY_NAME); + expect(entity.displayName).toBe(ENTITY_TEST_CONSTANTS.ENTITY_DISPLAY_NAME); + expect(entity.description).toBe(ENTITY_TEST_CONSTANTS.ENTITY_DESCRIPTION); + expect(entity.fields).toBeDefined(); + expect(entity.fields.length).toBeGreaterThan(0); + }); + + it('should have all methods available', () => { + const entityData = createBasicEntity(); + const entity = createEntityWithMethods(entityData, mockService); + + expect(typeof entity.insert).toBe('function'); + expect(typeof entity.update).toBe('function'); + expect(typeof entity.delete).toBe('function'); + expect(typeof entity.getRecords).toBe('function'); + }); + }); +}); + diff --git a/tests/unit/models/data-fabric/index.ts b/tests/unit/models/data-fabric/index.ts new file mode 100644 index 00000000..91d0ee9a --- /dev/null +++ b/tests/unit/models/data-fabric/index.ts @@ -0,0 +1,6 @@ +/** + * Data Fabric models test exports + */ + +export * from './entities.test'; + diff --git a/tests/unit/services/data-fabric/entities.test.ts b/tests/unit/services/data-fabric/entities.test.ts new file mode 100644 index 00000000..3e1c2b89 --- /dev/null +++ b/tests/unit/services/data-fabric/entities.test.ts @@ -0,0 +1,686 @@ +// ===== IMPORTS ===== +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { EntityService } from '../../../../src/services/data-fabric/entities'; +import { ApiClient } from '../../../../src/core/http/api-client'; +import { PaginationHelpers } from '../../../../src/utils/pagination/helpers'; +import { + createMockEntityResponse, + createMockEntities, + createMockEntityRecords, + createMockInsertResponse, + createMockUpdateResponse, + createMockDeleteResponse, + createMockEntityWithExternalFields, + createMockEntityWithNestedReferences, + createMockEntityWithSqlFieldTypes +} from '../../../utils/mocks/entities'; +import { createServiceTestDependencies, createMockApiClient } from '../../../utils/setup'; +import { createMockError } from '../../../utils/mocks/core'; +import type { + EntityGetRecordsByIdOptions, + EntityInsertOptions, + EntityUpdateOptions, + EntityDeleteOptions, + EntityRecord +} from '../../../../src/models/data-fabric/entities.types'; +import { ENTITY_TEST_CONSTANTS } from '../../../utils/constants/entities'; +import { TEST_CONSTANTS } from '../../../utils/constants/common'; +import { DATA_FABRIC_ENDPOINTS } from '../../../../src/utils/constants/endpoints'; + +// ===== MOCKING ===== +// Mock the dependencies +vi.mock('../../../../src/core/http/api-client'); + +// Import mock objects using vi.hoisted() - this ensures they're available before vi.mock() calls +const mocks = vi.hoisted(() => { + // Import/re-export the mock utilities from core + return import('../../../utils/mocks/core'); +}); + +// Setup mocks at module level +// NOTE: We do NOT mock transformData - we want to test the actual transformation logic! +vi.mock('../../../../src/utils/pagination/helpers', async () => (await mocks).mockPaginationHelpers); + +// ===== TEST SUITE ===== +describe('EntityService Unit Tests', () => { + let entityService: EntityService; + let mockApiClient: any; + + beforeEach(() => { + // Create mock instances using centralized setup + const { config, executionContext, tokenManager } = createServiceTestDependencies(); + mockApiClient = createMockApiClient(); + + // Mock the ApiClient constructor + vi.mocked(ApiClient).mockImplementation(() => mockApiClient); + + // Reset pagination helpers mock before each test + vi.mocked(PaginationHelpers.getAll).mockReset(); + + entityService = new EntityService(config, executionContext, tokenManager); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('getById', () => { + it('should get entity by ID successfully with all fields mapped correctly', async () => { + const mockResponse = createMockEntityResponse(); + mockApiClient.get.mockResolvedValue(mockResponse); + + const result = await entityService.getById(ENTITY_TEST_CONSTANTS.ENTITY_ID); + + // Verify the result + expect(result).toBeDefined(); + expect(result.id).toBe(ENTITY_TEST_CONSTANTS.ENTITY_ID); + expect(result.name).toBe(ENTITY_TEST_CONSTANTS.ENTITY_NAME); + expect(result.displayName).toBe(ENTITY_TEST_CONSTANTS.ENTITY_DISPLAY_NAME); + expect(result.fields).toBeDefined(); + expect(result.fields.length).toBe(3); + + // Verify the API call has correct endpoint + expect(mockApiClient.get).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.ENTITY.GET_BY_ID(ENTITY_TEST_CONSTANTS.ENTITY_ID), + {} + ); + + // Verify entity has methods attached + expect(typeof result.insert).toBe('function'); + expect(typeof result.update).toBe('function'); + expect(typeof result.delete).toBe('function'); + expect(typeof result.getRecords).toBe('function'); + }); + + it('should get entity with external fields successfully and transform field metadata', async () => { + const mockResponse = createMockEntityWithExternalFields(); + mockApiClient.get.mockResolvedValue(mockResponse); + + const result = await entityService.getById(ENTITY_TEST_CONSTANTS.ENTITY_ID); + + expect(result).toBeDefined(); + expect(result.externalFields).toBeDefined(); + expect(result.externalFields?.length).toBeGreaterThan(0); + expect(result.externalFields![0].externalObjectDetail).toBeDefined(); + expect(result.externalFields![0].externalConnectionDetail).toBeDefined(); + + // Verify external field metadata field name transformation (fieldDefinition → fieldMetaData) + const externalField = result.externalFields![0].fields![0]; + expect(externalField.fieldMetaData).toBeDefined(); + expect(externalField.fieldMetaData.id).toBe(ENTITY_TEST_CONSTANTS.FIELD_ID); + expect(externalField.fieldMetaData.name).toBe(ENTITY_TEST_CONSTANTS.FIELD_EXTERNAL_FIELD); + + // NOTE: External fields currently do NOT transform SQL types to friendly names + // They only transform field names (sqlType → fieldDataType, createTime → createdTime) + // This tests the ACTUAL current behavior + expect(externalField.fieldMetaData.fieldDataType).toBeDefined(); + expect(externalField.fieldMetaData.fieldDataType.name).toBe(ENTITY_TEST_CONSTANTS.FIELD_TYPE_NVARCHAR); // Stays as SQL type + }); + + it('should transform nested reference fields correctly', async () => { + const mockResponse = createMockEntityWithNestedReferences(); + mockApiClient.get.mockResolvedValue(mockResponse); + + const result = await entityService.getById(ENTITY_TEST_CONSTANTS.ENTITY_ID); + + expect(result).toBeDefined(); + expect(result.fields).toBeDefined(); + expect(result.fields.length).toBe(3); + + // Verify referenceEntity field is transformed + const refEntityField = result.fields.find(f => f.name === 'customerId'); + expect(refEntityField).toBeDefined(); + expect(refEntityField?.referenceEntity).toBeDefined(); + expect(refEntityField?.referenceEntity?.id).toBe('ref-entity-id'); + expect(refEntityField?.referenceEntity?.name).toBe(ENTITY_TEST_CONSTANTS.REFERENCE_ENTITY_CUSTOMER); + + // Verify referenceChoiceSet field is transformed + const refChoiceSetField = result.fields.find(f => f.name === 'status'); + expect(refChoiceSetField).toBeDefined(); + expect(refChoiceSetField?.referenceChoiceSet).toBeDefined(); + expect(refChoiceSetField?.referenceChoiceSet?.id).toBe('ref-choiceset-id'); + expect(refChoiceSetField?.referenceChoiceSet?.name).toBe(ENTITY_TEST_CONSTANTS.REFERENCE_CHOICESET_STATUS); + + // Verify referenceField.definition is transformed + const refFieldField = result.fields.find(f => f.name === 'relatedField'); + expect(refFieldField).toBeDefined(); + expect(refFieldField?.referenceField).toBeDefined(); + expect(refFieldField?.referenceField?.definition).toBeDefined(); + expect(refFieldField?.referenceField?.definition?.id).toBe('ref-field-def-id'); + expect(refFieldField?.referenceField?.definition?.name).toBe(ENTITY_TEST_CONSTANTS.REFERENCE_FIELD_DEF); + }); + + it('should transform SQL field types to friendly names', async () => { + const mockResponse = createMockEntityWithSqlFieldTypes(); + mockApiClient.get.mockResolvedValue(mockResponse); + + const result = await entityService.getById(ENTITY_TEST_CONSTANTS.ENTITY_ID); + + expect(result).toBeDefined(); + expect(result.fields).toBeDefined(); + expect(result.fields.length).toBe(6); + + // Verify UNIQUEIDENTIFIER -> UUID + const uuidField = result.fields.find(f => f.name === 'id'); + expect(uuidField?.fieldDataType.name).toBe(ENTITY_TEST_CONSTANTS.FIELD_TYPE_UUID); + + // Verify NVARCHAR -> STRING + const stringField = result.fields.find(f => f.name === 'name'); + expect(stringField?.fieldDataType.name).toBe(ENTITY_TEST_CONSTANTS.FIELD_TYPE_STRING); + expect(stringField?.fieldDataType.lengthLimit).toBe(255); + + // Verify INT -> INTEGER + const intField = result.fields.find(f => f.name === 'age'); + expect(intField?.fieldDataType.name).toBe(ENTITY_TEST_CONSTANTS.FIELD_TYPE_INTEGER); + + // Verify DATETIME2 -> DATETIME + const datetimeField = result.fields.find(f => f.name === 'createdDate'); + expect(datetimeField?.fieldDataType.name).toBe(ENTITY_TEST_CONSTANTS.FIELD_TYPE_DATETIME); + + // Verify BIT -> BOOLEAN + const boolField = result.fields.find(f => f.name === 'isActive'); + expect(boolField?.fieldDataType.name).toBe(ENTITY_TEST_CONSTANTS.FIELD_TYPE_BOOLEAN); + + // Verify DECIMAL -> DECIMAL (stays the same) + const decimalField = result.fields.find(f => f.name === 'price'); + expect(decimalField?.fieldDataType.name).toBe(ENTITY_TEST_CONSTANTS.FIELD_TYPE_DECIMAL); + expect(decimalField?.fieldDataType.decimalPrecision).toBe(2); + }); + + it('should handle API errors', async () => { + const error = createMockError(TEST_CONSTANTS.ERROR_MESSAGE); + mockApiClient.get.mockRejectedValue(error); + + await expect(entityService.getById(ENTITY_TEST_CONSTANTS.ENTITY_ID)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); + + describe('getAll', () => { + it('should return all entities with methods attached', async () => { + const mockEntities = createMockEntities(3); + mockApiClient.get.mockResolvedValue(mockEntities); + + const result = await entityService.getAll(); + + // Verify the result + expect(result).toBeDefined(); + expect(result.length).toBe(3); + + // Verify each entity has methods + result.forEach(entity => { + expect(typeof entity.insert).toBe('function'); + expect(typeof entity.update).toBe('function'); + expect(typeof entity.delete).toBe('function'); + expect(typeof entity.getRecords).toBe('function'); + }); + + // Verify the API call + expect(mockApiClient.get).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL, + {} + ); + + expect(result[0].fields).toBeDefined(); + expect(result[0].fields.length).toBe(3); + expect(result[0].fields[0].name).toBe('id'); + expect(result[0].fields[0].fieldDataType.name).toBe(ENTITY_TEST_CONSTANTS.FIELD_TYPE_UUID); + expect(result[0].fields[1].name).toBe('name'); + expect(result[0].fields[1].fieldDataType.name).toBe(ENTITY_TEST_CONSTANTS.FIELD_TYPE_STRING); + expect(result[0].fields[2].name).toBe('age'); + expect(result[0].fields[2].fieldDataType.name).toBe(ENTITY_TEST_CONSTANTS.FIELD_TYPE_INTEGER); + }); + + it('should apply EntityMap transformations correctly to all entities', async () => { + // Create mock with RAW API field names (before transformation) + const mockEntitiesRaw = createMockEntities(2); + mockApiClient.get.mockResolvedValue(mockEntitiesRaw); + + const result = await entityService.getAll(); + + expect(result).toBeDefined(); + expect(result.length).toBe(2); + + result.forEach(entity => { + // Verify EntityMap transformations on entity level + // createTime -> createdTime + expect(entity.createdTime).toBeDefined(); + expect(entity.createdTime).toBe(ENTITY_TEST_CONSTANTS.CREATED_TIME); + expect(entity).not.toHaveProperty('createTime'); // Raw field should not exist + + // updateTime -> updatedTime + expect(entity.updatedTime).toBeDefined(); + expect(entity.updatedTime).toBe(ENTITY_TEST_CONSTANTS.UPDATED_TIME); + expect(entity).not.toHaveProperty('updateTime'); // Raw field should not exist + + // Verify field-level transformations + entity.fields.forEach(field => { + // sqlType -> fieldDataType + expect(field.fieldDataType).toBeDefined(); + expect(field.fieldDataType.name).toBeDefined(); + expect(field).not.toHaveProperty('sqlType'); // Raw field should not exist + + // Use type assertion to check for any remaining raw field names + const fieldAsAny = field as any; + expect(fieldAsAny.fieldDefinition).toBeUndefined(); // Raw field should not exist + expect(fieldAsAny.createTime).toBeUndefined(); // Raw field should not exist + expect(fieldAsAny.updateTime).toBeUndefined(); // Raw field should not exist + }); + }); + + // Verify the API call + expect(mockApiClient.get).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL, + {} + ); + }); + + it('should handle API errors', async () => { + const error = createMockError(TEST_CONSTANTS.ERROR_MESSAGE); + mockApiClient.get.mockRejectedValue(error); + + await expect(entityService.getAll()).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); + + describe('getRecordsById', () => { + beforeEach(() => { + // Reset the mock before each test + vi.mocked(PaginationHelpers.getAll).mockReset(); + }); + + it('should return all records without pagination', async () => { + const mockRecords = createMockEntityRecords(5); + const mockResponse = { + items: mockRecords, + totalCount: 5 + }; + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const result = await entityService.getRecordsById(ENTITY_TEST_CONSTANTS.ENTITY_ID); + + // Verify PaginationHelpers.getAll was called with correct parameters + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.any(Function), + transformFn: expect.any(Function), + pagination: expect.any(Object), + excludeFromPrefix: ['expansionLevel'] + }), + undefined + ); + + expect(result).toEqual(mockResponse); + expect(result.items).toHaveLength(5); + }); + + it('should return paginated records when pagination options provided', async () => { + const mockRecords = createMockEntityRecords(10); + const mockResponse = { + items: mockRecords, + totalCount: 100, + hasNextPage: true, + nextCursor: TEST_CONSTANTS.NEXT_CURSOR, + previousCursor: null, + currentPage: 1, + totalPages: 10 + }; + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const options: EntityGetRecordsByIdOptions = { + pageSize: TEST_CONSTANTS.PAGE_SIZE + } as EntityGetRecordsByIdOptions; + + const result = await entityService.getRecordsById(ENTITY_TEST_CONSTANTS.ENTITY_ID, options) as any; + + // Verify PaginationHelpers.getAll was called with correct parameters + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.any(Function), + transformFn: expect.any(Function), + pagination: expect.any(Object) + }), + expect.objectContaining({ + pageSize: TEST_CONSTANTS.PAGE_SIZE + }) + ); + + expect(result).toEqual(mockResponse); + expect(result.hasNextPage).toBe(true); + }); + + it('should handle expansion level option', async () => { + // With expansionLevel, reference fields should be expanded to objects + const mockRecords = createMockEntityRecords(3, { + expansionLevel: ENTITY_TEST_CONSTANTS.EXPANSION_LEVEL + }); + const mockResponse = { + items: mockRecords, + totalCount: 3 + }; + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const options: EntityGetRecordsByIdOptions = { + expansionLevel: ENTITY_TEST_CONSTANTS.EXPANSION_LEVEL + } as EntityGetRecordsByIdOptions; + + await entityService.getRecordsById(ENTITY_TEST_CONSTANTS.ENTITY_ID, options); + + // Verify PaginationHelpers.getAll was called with correct parameters + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.any(Function), + transformFn: expect.any(Function), + pagination: expect.any(Object), + excludeFromPrefix: ['expansionLevel'] + }), + expect.objectContaining({ + expansionLevel: ENTITY_TEST_CONSTANTS.EXPANSION_LEVEL + }) + ); + }); + + it('should handle API errors', async () => { + const error = createMockError(TEST_CONSTANTS.ERROR_MESSAGE); + vi.mocked(PaginationHelpers.getAll).mockRejectedValue(error); + + await expect(entityService.getRecordsById(ENTITY_TEST_CONSTANTS.ENTITY_ID)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); + + describe('insertById', () => { + it('should insert records successfully', async () => { + const testData = [ + ENTITY_TEST_CONSTANTS.TEST_RECORD_DATA, + ENTITY_TEST_CONSTANTS.TEST_RECORD_DATA_2 + ]; + + const mockResponse = createMockInsertResponse(testData); + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await entityService.insertById(ENTITY_TEST_CONSTANTS.ENTITY_ID, testData); + + // Verify the result + expect(result).toBeDefined(); + expect(result.successRecords).toHaveLength(2); + expect(result.failureRecords).toHaveLength(0); + // Verify the response contains the data we sent (with IDs added) + expect(result.successRecords[0].name).toBe(testData[0].name); + expect(result.successRecords[0].age).toBe(testData[0].age); + expect(result.successRecords[0]).toHaveProperty('id'); + expect(result.successRecords[1].name).toBe(testData[1].name); + expect(result.successRecords[1]).toHaveProperty('id'); + + // Verify the API call has correct endpoint and body + expect(mockApiClient.post).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.ENTITY.INSERT_BY_ID(ENTITY_TEST_CONSTANTS.ENTITY_ID), + testData, + expect.objectContaining({ + params: expect.any(Object) + }) + ); + }); + + it('should insert records with options', async () => { + const testData = [{ + ...ENTITY_TEST_CONSTANTS.TEST_RECORD_DATA, + recordOwner: ENTITY_TEST_CONSTANTS.USER_ID, + createdBy: ENTITY_TEST_CONSTANTS.USER_ID + }]; + const options: EntityInsertOptions = { + expansionLevel: ENTITY_TEST_CONSTANTS.EXPANSION_LEVEL, + failOnFirst: ENTITY_TEST_CONSTANTS.FAIL_ON_FIRST + } as EntityInsertOptions; + + // With expansionLevel, reference fields should be expanded in the response + const mockResponse = createMockInsertResponse(testData, { + expansionLevel: ENTITY_TEST_CONSTANTS.EXPANSION_LEVEL + }); + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await entityService.insertById(ENTITY_TEST_CONSTANTS.ENTITY_ID, testData, options); + + // Verify options are passed in params + expect(mockApiClient.post).toHaveBeenCalledWith( + expect.any(String), + expect.any(Array), + expect.objectContaining({ + params: expect.objectContaining({ + expansionLevel: ENTITY_TEST_CONSTANTS.EXPANSION_LEVEL, + failOnFirst: ENTITY_TEST_CONSTANTS.FAIL_ON_FIRST + }) + }) + ); + + // Verify reference fields are expanded in the response + expect(result.successRecords[0].recordOwner).toEqual({ id: ENTITY_TEST_CONSTANTS.USER_ID }); + expect(result.successRecords[0].createdBy).toEqual({ id: ENTITY_TEST_CONSTANTS.USER_ID }); + }); + + it('should handle partial insert failures', async () => { + const testData = [ + ENTITY_TEST_CONSTANTS.TEST_RECORD_DATA, + { name: ENTITY_TEST_CONSTANTS.TEST_INVALID_RECORD_NAME, age: null } // Invalid data + ]; + + // First record succeeds, second fails (1 success, 1 failure from testData) + const mockResponse = createMockInsertResponse(testData, { successCount: 1 }); + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await entityService.insertById(ENTITY_TEST_CONSTANTS.ENTITY_ID, testData); + + expect(result.successRecords).toHaveLength(1); + expect(result.failureRecords).toHaveLength(1); + expect(result.failureRecords[0]).toHaveProperty('error'); + expect(result.failureRecords[0]).toHaveProperty('record'); + // Verify the failure contains the record we tried to insert + expect(result.failureRecords[0].record).toEqual(testData[1]); + // Verify the success record has the data plus generated ID + expect(result.successRecords[0].name).toBe(testData[0].name); + expect(result.successRecords[0].age).toBe(testData[0].age); + expect(result.successRecords[0]).toHaveProperty('id'); + }); + + it('should handle API errors', async () => { + const error = createMockError(TEST_CONSTANTS.ERROR_MESSAGE); + mockApiClient.post.mockRejectedValue(error); + + await expect(entityService.insertById( + ENTITY_TEST_CONSTANTS.ENTITY_ID, + [ENTITY_TEST_CONSTANTS.TEST_RECORD_DATA] + )).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); + + describe('updateById', () => { + it('should update records successfully', async () => { + const testData: EntityRecord[] = [ + { id: ENTITY_TEST_CONSTANTS.RECORD_ID, name: ENTITY_TEST_CONSTANTS.TEST_JOHN_UPDATED_NAME, age: ENTITY_TEST_CONSTANTS.TEST_JOHN_UPDATED_AGE }, + { id: ENTITY_TEST_CONSTANTS.RECORD_ID_2, name: ENTITY_TEST_CONSTANTS.TEST_JANE_UPDATED_NAME, age: ENTITY_TEST_CONSTANTS.TEST_JANE_UPDATED_AGE } + ]; + + const mockResponse = createMockUpdateResponse(testData); + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await entityService.updateById(ENTITY_TEST_CONSTANTS.ENTITY_ID, testData); + + // Verify the result + expect(result).toBeDefined(); + expect(result.successRecords).toHaveLength(2); + expect(result.failureRecords).toHaveLength(0); + // Verify the response contains the data we sent + expect(result.successRecords).toEqual(testData); + + // Verify the API call has correct endpoint and body + expect(mockApiClient.post).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.ENTITY.UPDATE_BY_ID(ENTITY_TEST_CONSTANTS.ENTITY_ID), + testData, + expect.objectContaining({ + params: expect.any(Object) + }) + ); + }); + + it('should update records with options', async () => { + const testData: EntityRecord[] = [ + { + id: ENTITY_TEST_CONSTANTS.RECORD_ID, + name: ENTITY_TEST_CONSTANTS.TEST_JOHN_UPDATED_NAME, + age: ENTITY_TEST_CONSTANTS.TEST_JOHN_UPDATED_AGE, + recordOwner: ENTITY_TEST_CONSTANTS.USER_ID, + updatedBy: ENTITY_TEST_CONSTANTS.USER_ID + } + ]; + const options: EntityUpdateOptions = { + expansionLevel: ENTITY_TEST_CONSTANTS.EXPANSION_LEVEL, + failOnFirst: ENTITY_TEST_CONSTANTS.FAIL_ON_FIRST + } as EntityUpdateOptions; + + // With expansionLevel, reference fields should be expanded in the response + const mockResponse = createMockUpdateResponse(testData, { + expansionLevel: ENTITY_TEST_CONSTANTS.EXPANSION_LEVEL + }); + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await entityService.updateById(ENTITY_TEST_CONSTANTS.ENTITY_ID, testData, options); + + // Verify options are passed in params + expect(mockApiClient.post).toHaveBeenCalledWith( + expect.any(String), + expect.any(Array), + expect.objectContaining({ + params: expect.objectContaining({ + expansionLevel: ENTITY_TEST_CONSTANTS.EXPANSION_LEVEL, + failOnFirst: ENTITY_TEST_CONSTANTS.FAIL_ON_FIRST + }) + }) + ); + + // Verify reference fields are expanded in the response + expect(result.successRecords[0].recordOwner).toEqual({ id: ENTITY_TEST_CONSTANTS.USER_ID }); + expect(result.successRecords[0].updatedBy).toEqual({ id: ENTITY_TEST_CONSTANTS.USER_ID }); + }); + + it('should handle partial update failures', async () => { + const testData: EntityRecord[] = [ + { id: ENTITY_TEST_CONSTANTS.RECORD_ID, name: ENTITY_TEST_CONSTANTS.TEST_VALID_UPDATE_NAME }, + { id: ENTITY_TEST_CONSTANTS.TEST_INVALID_ID, name: ENTITY_TEST_CONSTANTS.TEST_INVALID_UPDATE_NAME } + ]; + + // First record succeeds, second fails (1 success, 1 failure from testData) + const mockResponse = createMockUpdateResponse(testData, { successCount: 1 }); + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await entityService.updateById(ENTITY_TEST_CONSTANTS.ENTITY_ID, testData); + + expect(result.successRecords).toHaveLength(1); + expect(result.failureRecords).toHaveLength(1); + expect(result.failureRecords[0]).toHaveProperty('error'); + expect(result.failureRecords[0]).toHaveProperty('record'); + // Verify the failure contains the record we tried to update + expect(result.failureRecords[0].record).toEqual(testData[1]); + expect(result.successRecords[0]).toEqual(testData[0]); + }); + + it('should handle API errors', async () => { + const error = createMockError(TEST_CONSTANTS.ERROR_MESSAGE); + mockApiClient.post.mockRejectedValue(error); + + await expect(entityService.updateById( + ENTITY_TEST_CONSTANTS.ENTITY_ID, + [{ id: ENTITY_TEST_CONSTANTS.RECORD_ID, name: ENTITY_TEST_CONSTANTS.TEST_UPDATED_NAME }] + )).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); + + describe('deleteById', () => { + it('should delete records successfully', async () => { + const recordIds = [ + ENTITY_TEST_CONSTANTS.RECORD_ID, + ENTITY_TEST_CONSTANTS.RECORD_ID_2 + ]; + + const mockResponse = createMockDeleteResponse(recordIds); + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await entityService.deleteById(ENTITY_TEST_CONSTANTS.ENTITY_ID, recordIds); + + // Verify the result + expect(result).toBeDefined(); + expect(result.successRecords).toHaveLength(2); + expect(result.failureRecords).toHaveLength(0); + // Verify the response contains the IDs we sent + expect(result.successRecords[0].id).toBe(recordIds[0]); + expect(result.successRecords[1].id).toBe(recordIds[1]); + + // Verify the API call has correct endpoint and body + expect(mockApiClient.post).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.ENTITY.DELETE_BY_ID(ENTITY_TEST_CONSTANTS.ENTITY_ID), + recordIds, + expect.objectContaining({ + params: expect.any(Object) + }) + ); + }); + + it('should delete records with options', async () => { + const recordIds = [ENTITY_TEST_CONSTANTS.RECORD_ID]; + const options: EntityDeleteOptions = { + failOnFirst: ENTITY_TEST_CONSTANTS.FAIL_ON_FIRST + } as EntityDeleteOptions; + + const mockResponse = createMockDeleteResponse(recordIds); + mockApiClient.post.mockResolvedValue(mockResponse); + + await entityService.deleteById(ENTITY_TEST_CONSTANTS.ENTITY_ID, recordIds, options); + + // Verify options are passed in params + expect(mockApiClient.post).toHaveBeenCalledWith( + expect.any(String), + expect.any(Array), + expect.objectContaining({ + params: expect.objectContaining({ + failOnFirst: ENTITY_TEST_CONSTANTS.FAIL_ON_FIRST + }) + }) + ); + }); + + it('should handle partial delete failures', async () => { + const recordIds = [ + ENTITY_TEST_CONSTANTS.RECORD_ID, + ENTITY_TEST_CONSTANTS.TEST_INVALID_ID + ]; + + // First record deleted successfully, second fails (1 success, 1 failure from recordIds) + const mockResponse = createMockDeleteResponse(recordIds, { successCount: 1 }); + mockApiClient.post.mockResolvedValue(mockResponse); + + const result = await entityService.deleteById(ENTITY_TEST_CONSTANTS.ENTITY_ID, recordIds); + + expect(result.successRecords).toHaveLength(1); + expect(result.failureRecords).toHaveLength(1); + expect(result.failureRecords[0]).toHaveProperty('error'); + // Verify the failure contains the ID we tried to delete + expect(result.failureRecords[0].record?.id).toBe(recordIds[1]); + // Verify the success record contains the ID we deleted + expect(result.successRecords[0].id).toBe(recordIds[0]); + }); + + it('should handle API errors', async () => { + const error = createMockError(TEST_CONSTANTS.ERROR_MESSAGE); + mockApiClient.post.mockRejectedValue(error); + + await expect(entityService.deleteById( + ENTITY_TEST_CONSTANTS.ENTITY_ID, + [ENTITY_TEST_CONSTANTS.RECORD_ID] + )).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); +}); + diff --git a/tests/unit/services/data-fabric/index.ts b/tests/unit/services/data-fabric/index.ts new file mode 100644 index 00000000..ea061d20 --- /dev/null +++ b/tests/unit/services/data-fabric/index.ts @@ -0,0 +1,6 @@ +/** + * Data Fabric services test exports + */ + +export * from './entities.test'; + diff --git a/tests/utils/constants/common.ts b/tests/utils/constants/common.ts index 79a11ebd..7e5061ed 100644 --- a/tests/utils/constants/common.ts +++ b/tests/utils/constants/common.ts @@ -15,6 +15,12 @@ export const TEST_CONSTANTS = { PAGE_SIZE: 10, ERROR_MESSAGE: 'API Error', + // User Information + USER_EMAIL: 'testuser@uipath.com', + + // Pagination Values + NEXT_CURSOR: 'next-cursor', + // Base URLs and Endpoints BASE_URL: 'https://test.uipath.com', CLIENT_ID: 'test-client-id', diff --git a/tests/utils/constants/entities.ts b/tests/utils/constants/entities.ts new file mode 100644 index 00000000..1982d16e --- /dev/null +++ b/tests/utils/constants/entities.ts @@ -0,0 +1,88 @@ +/** + * Entity service test constants + * Entity-specific constants only + */ + +export const ENTITY_TEST_CONSTANTS = { + // Entity IDs + ENTITY_ID: 'e1234567-e89b-12d3-a456-426614174000', + + // Record IDs + RECORD_ID: 'r1234567-e89b-12d3-a456-426614174000', + RECORD_ID_2: 'r2234567-e89b-12d3-a456-426614174001', + + // Field IDs + FIELD_ID: 'f1234567-e89b-12d3-a456-426614174000', + FIELD_ID_NAME: 'f2234567-e89b-12d3-a456-426614174001', + FIELD_ID_AGE: 'f3234567-e89b-12d3-a456-426614174002', + + // Entity Metadata + ENTITY_NAME: 'Customer', + ENTITY_DISPLAY_NAME: 'Customer Entity', + ENTITY_DESCRIPTION: 'Customer entity for testing', + + // Field Names + FIELD_NAME: 'name', + FIELD_AGE: 'age', + FIELD_EXTERNAL_FIELD: 'externalField', + + // Reference Entity Names + REFERENCE_ENTITY_CUSTOMER: 'Customer', + REFERENCE_CHOICESET_STATUS: 'StatusChoiceSet', + REFERENCE_FIELD_DEF: 'relatedFieldDef', + + // Field Data Type Names (for assertions) + FIELD_TYPE_UUID: 'UUID', + FIELD_TYPE_STRING: 'STRING', + FIELD_TYPE_INTEGER: 'INTEGER', + FIELD_TYPE_DATETIME: 'DATETIME', + FIELD_TYPE_BOOLEAN: 'BOOLEAN', + FIELD_TYPE_DECIMAL: 'DECIMAL', + FIELD_TYPE_NVARCHAR: 'NVARCHAR', // SQL type + + // User Information + USER_ID: 'u1234567-e89b-12d3-a456-426614174000', + + // Timestamps + CREATED_TIME: '2025-01-15T10:00:00Z', + UPDATED_TIME: '2025-01-15T12:00:00Z', + + // Test Data + TEST_RECORD_DATA: { + name: 'John Doe', + age: 30, + email: 'john@example.com' + }, + + TEST_RECORD_DATA_2: { + name: 'Jane Smith', + age: 25, + email: 'jane@example.com' + }, + + // Test Data Values + TEST_INVALID_RECORD_NAME: 'Invalid', + TEST_VALID_UPDATE_NAME: 'Valid Update', + TEST_INVALID_UPDATE_NAME: 'Invalid Update', + TEST_JOHN_UPDATED_NAME: 'John Updated', + TEST_JANE_UPDATED_NAME: 'Jane Updated', + TEST_JOHN_UPDATED_AGE: 31, + TEST_JANE_UPDATED_AGE: 26, + TEST_UPDATED_NAME: 'Updated', + TEST_INVALID_ID: 'invalid-id', + + // Operation Options + EXPANSION_LEVEL: 1, + FAIL_ON_FIRST: true, + + // External Connection + EXTERNAL_CONNECTION_ID: 'c1234567-e89b-12d3-a456-426614174000', + EXTERNAL_OBJECT_ID: 'o1234567-e89b-12d3-a456-426614174000', + EXTERNAL_FIELD_MAPPING_ID: 'm1234567-e89b-12d3-a456-426614174000', + + // Error Messages + ERROR_MESSAGE: 'Record not found', + ERROR_MESSAGE_INSERT_UNIQUENESS: 'Insert data failed. Value uniqueness violation.', + ERROR_MESSAGE_ENTITY_ID_UNDEFINED: 'Entity ID is undefined', +} as const; + diff --git a/tests/utils/constants/index.ts b/tests/utils/constants/index.ts index 34981c15..cca8f3ba 100644 --- a/tests/utils/constants/index.ts +++ b/tests/utils/constants/index.ts @@ -5,4 +5,5 @@ export * from './common'; export * from './maestro'; export * from './tasks'; +export * from './entities'; diff --git a/tests/utils/mocks/entities.ts b/tests/utils/mocks/entities.ts new file mode 100644 index 00000000..14790550 --- /dev/null +++ b/tests/utils/mocks/entities.ts @@ -0,0 +1,651 @@ +/** + * Entities service mock utilities - Entity-specific mocks only + * Uses generic utilities from core.ts for base functionality + */ + +import { + EntityType, + EntityFieldDataType, + ReferenceType, + FieldDisplayType, + DataDirectionType, + RawEntityGetResponse, + EntityRecord, + EntityInsertResponse, + EntityUpdateResponse, + EntityDeleteResponse +} from '../../../src/models/data-fabric/entities.types'; +import { createMockBaseResponse, createMockCollection } from './core'; +import { ENTITY_TEST_CONSTANTS } from '../constants/entities'; +import { TEST_CONSTANTS } from '../constants/common'; + +// Entity-Specific Mock Factories + +/** + * Creates a mock FieldMetaData object with RAW API response format + * This uses raw field names (sqlType, createTime, updateTime) that will be transformed by the service + * @param overrides - Optional overrides for specific fields + * @returns Mock FieldMetaData object as it comes from the API (before transformation) + */ +export const createMockFieldMetaData = (overrides: Partial = {}): any => { + return createMockBaseResponse({ + id: ENTITY_TEST_CONSTANTS.FIELD_ID, + name: ENTITY_TEST_CONSTANTS.FIELD_NAME, + isPrimaryKey: false, + isForeignKey: false, + isExternalField: false, + isHiddenField: false, + isUnique: false, + referenceType: ReferenceType.ManyToOne, + // RAW API field name: sqlType (will be transformed to fieldDataType) + sqlType: { + name: 'NVARCHAR', // Raw SQL type from API (will be transformed to STRING) + lengthLimit: 255 + }, + isRequired: false, + displayName: 'Name', + description: 'Name field', + // RAW API field names: createTime/updateTime (will be transformed to createdTime/updatedTime) + createTime: ENTITY_TEST_CONSTANTS.CREATED_TIME, + createdBy: ENTITY_TEST_CONSTANTS.USER_ID, + updateTime: ENTITY_TEST_CONSTANTS.UPDATED_TIME, + updatedBy: ENTITY_TEST_CONSTANTS.USER_ID, + isSystemField: false, + fieldDisplayType: FieldDisplayType.Basic, + isAttachment: false, + isRbacEnabled: false, + }, overrides); +}; + +/** + * Creates a mock Entity response with RAW API format (before transformation) + * Uses raw field names: sqlType, createTime, updateTime (not fieldDataType, createdTime, updatedTime) + * @param overrides - Optional overrides for specific fields + * @returns Mock Entity response object as it comes from the API (before transformation) + */ +export const createMockEntityResponse = (overrides: Partial = {}): any => { + return createMockBaseResponse({ + id: ENTITY_TEST_CONSTANTS.ENTITY_ID, + name: ENTITY_TEST_CONSTANTS.ENTITY_NAME, + displayName: ENTITY_TEST_CONSTANTS.ENTITY_DISPLAY_NAME, + entityType: EntityType.Entity, + description: ENTITY_TEST_CONSTANTS.ENTITY_DESCRIPTION, + fields: [ + createMockFieldMetaData({ + id: ENTITY_TEST_CONSTANTS.FIELD_ID, + name: 'id', + isPrimaryKey: true, + sqlType: { // RAW API field name (will be transformed to fieldDataType) + name: 'UNIQUEIDENTIFIER' // Raw SQL type (will be transformed to UUID) + }, + displayName: 'ID', + description: 'Primary key' + }), + createMockFieldMetaData({ + id: ENTITY_TEST_CONSTANTS.FIELD_ID_NAME, + name: ENTITY_TEST_CONSTANTS.FIELD_NAME, + sqlType: { // RAW API field name (will be transformed to fieldDataType) + name: 'NVARCHAR', // Raw SQL type (will be transformed to STRING) + lengthLimit: 255 + }, + displayName: 'Name', + description: 'Customer name' + }), + createMockFieldMetaData({ + id: ENTITY_TEST_CONSTANTS.FIELD_ID_AGE, + name: ENTITY_TEST_CONSTANTS.FIELD_AGE, + sqlType: { // RAW API field name (will be transformed to fieldDataType) + name: 'INT' // Raw SQL type (will be transformed to INTEGER) + }, + displayName: 'Age', + description: 'Customer age' + }) + ], + externalFields: [], + sourceJoinCriterias: [], + isRbacEnabled: false, + createdBy: ENTITY_TEST_CONSTANTS.USER_ID, + // RAW API field names: createTime/updateTime (will be transformed to createdTime/updatedTime) + createTime: ENTITY_TEST_CONSTANTS.CREATED_TIME, + updateTime: ENTITY_TEST_CONSTANTS.UPDATED_TIME, + updatedBy: ENTITY_TEST_CONSTANTS.USER_ID, + }, overrides); +}; + +/** + * Creates a basic entity for model tests with TRANSFORMED data (not raw API format) + * @param overrides - Optional overrides for specific fields + * @returns Basic entity response object with transformed field names + */ +export const createBasicEntity = (overrides: Partial = {}): RawEntityGetResponse => { + return createMockBaseResponse({ + id: ENTITY_TEST_CONSTANTS.ENTITY_ID, + name: ENTITY_TEST_CONSTANTS.ENTITY_NAME, + displayName: ENTITY_TEST_CONSTANTS.ENTITY_DISPLAY_NAME, + entityType: EntityType.Entity, + description: ENTITY_TEST_CONSTANTS.ENTITY_DESCRIPTION, + fields: [ + createMockBaseResponse({ + id: ENTITY_TEST_CONSTANTS.FIELD_ID, + name: 'id', + isPrimaryKey: true, + isForeignKey: false, + isExternalField: false, + isHiddenField: false, + isUnique: false, + referenceType: ReferenceType.ManyToOne, + fieldDataType: { // TRANSFORMED field name (model tests need this) + name: EntityFieldDataType.UUID // TRANSFORMED type (model tests need this) + }, + isRequired: true, + displayName: 'ID', + description: 'Primary key', + createdTime: ENTITY_TEST_CONSTANTS.CREATED_TIME, // TRANSFORMED field name + createdBy: ENTITY_TEST_CONSTANTS.USER_ID, + updatedTime: ENTITY_TEST_CONSTANTS.UPDATED_TIME, // TRANSFORMED field name + updatedBy: ENTITY_TEST_CONSTANTS.USER_ID, + isSystemField: false, + fieldDisplayType: FieldDisplayType.Basic, + isAttachment: false, + isRbacEnabled: false, + }), + createMockBaseResponse({ + id: ENTITY_TEST_CONSTANTS.FIELD_ID_NAME, + name: ENTITY_TEST_CONSTANTS.FIELD_NAME, + isPrimaryKey: false, + isForeignKey: false, + isExternalField: false, + isHiddenField: false, + isUnique: false, + referenceType: ReferenceType.ManyToOne, + fieldDataType: { // TRANSFORMED field name (model tests need this) + name: EntityFieldDataType.STRING, // TRANSFORMED type (model tests need this) + lengthLimit: 255 + }, + isRequired: false, + displayName: 'Name', + description: 'Customer name', + createdTime: ENTITY_TEST_CONSTANTS.CREATED_TIME, // TRANSFORMED field name + createdBy: ENTITY_TEST_CONSTANTS.USER_ID, + updatedTime: ENTITY_TEST_CONSTANTS.UPDATED_TIME, // TRANSFORMED field name + updatedBy: ENTITY_TEST_CONSTANTS.USER_ID, + isSystemField: false, + fieldDisplayType: FieldDisplayType.Basic, + isAttachment: false, + isRbacEnabled: false, + }), + createMockBaseResponse({ + id: ENTITY_TEST_CONSTANTS.FIELD_ID_AGE, + name: ENTITY_TEST_CONSTANTS.FIELD_AGE, + isPrimaryKey: false, + isForeignKey: false, + isExternalField: false, + isHiddenField: false, + isUnique: false, + referenceType: ReferenceType.ManyToOne, + fieldDataType: { // TRANSFORMED field name (model tests need this) + name: EntityFieldDataType.INTEGER // TRANSFORMED type (model tests need this) + }, + isRequired: false, + displayName: 'Age', + description: 'Customer age', + createdTime: ENTITY_TEST_CONSTANTS.CREATED_TIME, // TRANSFORMED field name + createdBy: ENTITY_TEST_CONSTANTS.USER_ID, + updatedTime: ENTITY_TEST_CONSTANTS.UPDATED_TIME, // TRANSFORMED field name + updatedBy: ENTITY_TEST_CONSTANTS.USER_ID, + isSystemField: false, + fieldDisplayType: FieldDisplayType.Basic, + isAttachment: false, + isRbacEnabled: false, + }) + ], + externalFields: [], + sourceJoinCriterias: [], + isRbacEnabled: false, + createdBy: ENTITY_TEST_CONSTANTS.USER_ID, + createdTime: ENTITY_TEST_CONSTANTS.CREATED_TIME, // TRANSFORMED field name + updatedTime: ENTITY_TEST_CONSTANTS.UPDATED_TIME, // TRANSFORMED field name + updatedBy: ENTITY_TEST_CONSTANTS.USER_ID, + }, overrides); +}; + +/** + * Creates a collection of mock entities + * @param count - Number of entities to create + * @returns Array of mock entities + */ +export const createMockEntities = (count: number): RawEntityGetResponse[] => { + return createMockCollection(count, (i) => + createMockEntityResponse({ + id: `e${i}234567-e89b-12d3-a456-42661417400${i}`, + name: `Entity${i + 1}`, + displayName: `Entity ${i + 1}`, + description: `Test entity ${i + 1}`, + }) + ); +}; + +/** + * Creates a mock EntityRecord with common reference fields + * @param overrides - Optional overrides for specific fields + * @returns Mock EntityRecord object + */ +export const createMockEntityRecord = (overrides: Partial = {}): EntityRecord => { + return createMockBaseResponse({ + id: ENTITY_TEST_CONSTANTS.RECORD_ID, + name: ENTITY_TEST_CONSTANTS.TEST_RECORD_DATA.name, + age: ENTITY_TEST_CONSTANTS.TEST_RECORD_DATA.age, + email: ENTITY_TEST_CONSTANTS.TEST_RECORD_DATA.email, + recordOwner: ENTITY_TEST_CONSTANTS.USER_ID, + createdBy: ENTITY_TEST_CONSTANTS.USER_ID, + updatedBy: ENTITY_TEST_CONSTANTS.USER_ID, + }, overrides); +}; + +/** + * Creates a collection of mock entity records + * @param count - Number of records to create + * @param options - Optional: expansionLevel to expand reference fields + * @returns Array of mock entity records + */ +export const createMockEntityRecords = ( + count: number, + options?: { expansionLevel?: number } +): EntityRecord[] => { + const records = createMockCollection(count, (i) => + createMockEntityRecord({ + id: `r${i}234567-e89b-12d3-a456-42661417400${i}`, + name: `Record ${i + 1}`, + age: 20 + i, + email: `record${i + 1}@example.com`, + }) + ); + + // If expansionLevel is specified, expand reference fields + if (options?.expansionLevel && options.expansionLevel > 0) { + return records.map(record => expandRecordReferenceFields(record)); + } + + return records; +}; + +/** + * Expands reference fields (like recordOwner, createdBy, updatedBy) from string IDs to objects + * This simulates the API behavior when expansionLevel > 0 + * @param record - Record with string reference fields + * @returns Record with expanded reference fields + */ +export const expandRecordReferenceFields = (record: EntityRecord): EntityRecord => { + const expanded: EntityRecord = { ...record }; + + // Expand reference fields that are typically string IDs + const referenceFields = ['recordOwner', 'createdBy', 'updatedBy']; + + referenceFields.forEach(field => { + if (expanded[field] && typeof expanded[field] === 'string') { + expanded[field] = { id: expanded[field] as string }; + } + }); + + return expanded; +}; + +/** + * Creates a mock EntityInsertResponse that echoes back the request data with generated IDs + * @param requestData - Array of records being inserted + * @param options - Optional: successCount to control partial failures, expansionLevel to expand reference fields + * @returns Mock EntityInsertResponse + * + * @example + * // All records succeed + * const response = createMockInsertResponse([ + * { name: 'John', age: 30 }, + * { name: 'Jane', age: 25 } + * ]); + * // Returns: { + * // successRecords: [ + * // { name: 'John', age: 30, id: 'generated-id-1' }, + * // { name: 'Jane', age: 25, id: 'generated-id-2' } + * // ], + * // failureRecords: [] + * // } + * + * // Partial failure - first record succeeds, second fails + * const response = createMockInsertResponse( + * [{ name: 'Valid' }, { name: 'Invalid', age: null }], + * { successCount: 1 } + * ); + * + * // With expansion level - reference fields are expanded + * const response = createMockInsertResponse( + * [{ name: 'John', recordOwner: 'user-id-123' }], + * { expansionLevel: 1 } + * ); + * // Returns: { successRecords: [{ name: 'John', recordOwner: { id: 'user-id-123' }, id: 'generated-id-1' }] } + */ +export const createMockInsertResponse = ( + requestData: Record[], + options?: { successCount?: number; expansionLevel?: number } +): EntityInsertResponse => { + const successCount = options?.successCount ?? requestData.length; + + let successRecords = requestData.slice(0, successCount).map((record, i) => ({ + ...record, + id: `generated-id-${i + 1}` + })); + + // If expansionLevel is specified, expand reference fields in the response + if (options?.expansionLevel && options.expansionLevel > 0) { + successRecords = successRecords.map(record => expandRecordReferenceFields(record)); + } + + const failureRecords = requestData.slice(successCount).map((record) => ({ + error: ENTITY_TEST_CONSTANTS.ERROR_MESSAGE_INSERT_UNIQUENESS, + record + })); + + return { successRecords, failureRecords }; +}; + +/** + * Creates a mock EntityUpdateResponse that echoes back the request data + * @param requestData - Array of records being updated + * @param options - Optional: successCount to control partial failures, expansionLevel to expand reference fields + * @returns Mock EntityUpdateResponse + * + * @example + * // All records succeed + * const response = createMockUpdateResponse([ + * { id: '123', name: 'John Updated', age: 31 }, + * { id: '456', name: 'Jane Updated', age: 26 } + * ]); + * // Returns: { + * // successRecords: [ + * // { id: '123', name: 'John Updated', age: 31 }, + * // { id: '456', name: 'Jane Updated', age: 26 } + * // ], + * // failureRecords: [] + * // } + * + * // Partial failure - first record succeeds, second fails + * const response = createMockUpdateResponse( + * [{ id: 'valid-id', name: 'Valid' }, { id: 'invalid-id', name: 'Invalid' }], + * { successCount: 1 } + * ); + * + * // With expansion level - reference fields are expanded + * const response = createMockUpdateResponse( + * [{ id: '123', name: 'John', recordOwner: 'user-id-123' }], + * { expansionLevel: 1 } + * ); + */ +export const createMockUpdateResponse = ( + requestData: EntityRecord[], + options?: { successCount?: number; expansionLevel?: number } +): EntityUpdateResponse => { + const successCount = options?.successCount ?? requestData.length; + + let successRecords = requestData.slice(0, successCount); + + // If expansionLevel is specified, expand reference fields in the response + if (options?.expansionLevel && options.expansionLevel > 0) { + successRecords = successRecords.map(record => expandRecordReferenceFields(record)); + } + + const failureRecords = requestData.slice(successCount).map((record, i) => ({ + error: `${ENTITY_TEST_CONSTANTS.ERROR_MESSAGE} for record ${successCount + i + 1}`, + record + })); + + return { successRecords, failureRecords }; +}; + +/** + * Creates a mock EntityDeleteResponse that echoes back the request IDs + * @param requestIds - Array of IDs being deleted + * @param options - Optional: successCount to control partial failures + * @returns Mock EntityDeleteResponse + * + * @example + * // All deletions succeed + * const response = createMockDeleteResponse(['123', '456']); + * // Returns: { + * // successRecords: [{ id: '123' }, { id: '456' }], + * // failureRecords: [] + * // } + * + * // Partial failure - first deletion succeeds, second fails + * const response = createMockDeleteResponse( + * ['valid-id', 'invalid-id'], + * { successCount: 1 } + * ); + */ +export const createMockDeleteResponse = ( + requestIds: string[], + options?: { successCount?: number } +): EntityDeleteResponse => { + const successCount = options?.successCount ?? requestIds.length; + + const successRecords = requestIds.slice(0, successCount).map(id => ({ id })); + + const failureRecords = requestIds.slice(successCount).map((id) => ({ + error: `${ENTITY_TEST_CONSTANTS.ERROR_MESSAGE} for id: ${id}`, + record: { id } + })); + + return { successRecords, failureRecords }; +}; + +/** + * Creates a mock entity with SQL field types (as returned by API) for testing field type mapping + * Uses RAW API field name: sqlType (not fieldDataType) + * @returns Mock entity with SQL field types + */ +export const createMockEntityWithSqlFieldTypes = (): any => { + return createMockEntityResponse({ + fields: [ + // UUID field (UNIQUEIDENTIFIER -> UUID) + { + ...createMockFieldMetaData({ + id: 'field-uuid-id', + name: 'id', + displayName: 'ID', + isPrimaryKey: true + }), + sqlType: { // RAW API field name (will be transformed to fieldDataType) + name: 'UNIQUEIDENTIFIER' // SQL type from API + } + }, + // String field (NVARCHAR -> STRING) + { + ...createMockFieldMetaData({ + id: 'field-string-id', + name: 'name', + displayName: 'Name' + }), + sqlType: { // RAW API field name (will be transformed to fieldDataType) + name: 'NVARCHAR', // SQL type from API + lengthLimit: 255 + } + }, + // Integer field (INT -> INTEGER) + { + ...createMockFieldMetaData({ + id: 'field-int-id', + name: 'age', + displayName: 'Age' + }), + sqlType: { // RAW API field name (will be transformed to fieldDataType) + name: 'INT' // SQL type from API + } + }, + // DateTime field (DATETIME2 -> DATETIME) + { + ...createMockFieldMetaData({ + id: 'field-datetime-id', + name: 'createdDate', + displayName: 'Created Date' + }), + sqlType: { // RAW API field name (will be transformed to fieldDataType) + name: 'DATETIME2' // SQL type from API + } + }, + // Boolean field (BIT -> BOOLEAN) + { + ...createMockFieldMetaData({ + id: 'field-bool-id', + name: 'isActive', + displayName: 'Is Active' + }), + sqlType: { // RAW API field name (will be transformed to fieldDataType) + name: 'BIT' // SQL type from API + } + }, + // Decimal field (DECIMAL -> DECIMAL) + { + ...createMockFieldMetaData({ + id: 'field-decimal-id', + name: 'price', + displayName: 'Price' + }), + sqlType: { // RAW API field name (will be transformed to fieldDataType) + name: 'DECIMAL', // SQL type from API + decimalPrecision: 2 + } + } + ] + }); +}; + +/** + * Creates a mock entity with nested reference fields for testing transformNestedReferences + * Uses RAW API field names: sqlType, createTime, updateTime + * @returns Mock entity with reference fields + */ +export const createMockEntityWithNestedReferences = (): any => { + return createMockEntityResponse({ + fields: [ + // Field with referenceEntity + createMockFieldMetaData({ + id: 'field-ref-entity-id', + name: 'customerId', + displayName: 'Customer', + fieldDisplayType: FieldDisplayType.Relationship, + referenceEntity: { + id: 'ref-entity-id', + name: 'Customer', + displayName: 'Customer Entity', + entityType: EntityType.Entity, + description: 'Referenced customer entity', + fields: [], + isRbacEnabled: false, + createdBy: TEST_CONSTANTS.USER_EMAIL, + createTime: ENTITY_TEST_CONSTANTS.CREATED_TIME // RAW API field name + } + }), + // Field with referenceChoiceSet + createMockFieldMetaData({ + id: 'field-ref-choiceset-id', + name: 'status', + displayName: 'Status', + fieldDisplayType: FieldDisplayType.ChoiceSetSingle, + referenceChoiceSet: { + id: 'ref-choiceset-id', + name: 'StatusChoiceSet', + displayName: 'Status Choice Set', + entityType: EntityType.ChoiceSet, + description: 'Status options', + fields: [], + isRbacEnabled: false, + createdBy: TEST_CONSTANTS.USER_EMAIL, + createTime: ENTITY_TEST_CONSTANTS.CREATED_TIME // RAW API field name + } + }), + // Field with referenceField.definition + createMockFieldMetaData({ + id: 'field-ref-field-id', + name: 'relatedField', + displayName: 'Related Field', + referenceField: { + id: 'ref-field-id', + // NOTE: referenceField has a 'definition' property containing raw field metadata + definition: { + id: 'ref-field-def-id', + name: 'relatedFieldDef', + displayName: 'Related Field Definition', + isPrimaryKey: false, + isForeignKey: false, + isExternalField: false, + isHiddenField: false, + isUnique: false, + referenceType: ReferenceType.ManyToOne, + sqlType: { // RAW API field name (will be transformed to fieldDataType) + name: 'NVARCHAR' // SQL type (will be transformed to STRING) + }, + isRequired: false, + description: 'Referenced field definition', + createTime: ENTITY_TEST_CONSTANTS.CREATED_TIME, // RAW API field name + createdBy: TEST_CONSTANTS.USER_EMAIL, + updateTime: ENTITY_TEST_CONSTANTS.UPDATED_TIME, // RAW API field name + isSystemField: false, + isAttachment: false, + isRbacEnabled: false + } + } + }) + ] + }); +}; + +/** + * Creates a mock entity with external fields for testing + * Uses RAW API field name: fieldDefinition (will be transformed to fieldMetaData) + * @returns Mock entity with external fields + */ +export const createMockEntityWithExternalFields = (): any => { + return createMockEntityResponse({ + externalFields: [ + { + fields: [ + { + // RAW API field name: fieldDefinition (will be transformed to fieldMetaData) + fieldDefinition: createMockFieldMetaData({ + id: ENTITY_TEST_CONSTANTS.FIELD_ID, + name: 'externalField', + isExternalField: true, + displayName: 'External Field' + }), + externalFieldMappingDetail: { + id: ENTITY_TEST_CONSTANTS.EXTERNAL_FIELD_MAPPING_ID, + externalFieldName: 'external_field', + externalFieldDisplayName: 'External Field', + externalObjectId: ENTITY_TEST_CONSTANTS.EXTERNAL_OBJECT_ID, + externalFieldType: 'STRING', + internalFieldId: ENTITY_TEST_CONSTANTS.FIELD_ID, + directionType: DataDirectionType.ReadAndWrite + } + } + ], + externalObjectDetail: { + id: ENTITY_TEST_CONSTANTS.EXTERNAL_OBJECT_ID, + externalObjectName: 'ExternalObject', + externalObjectDisplayName: 'External Object', + primaryKey: 'id', + externalConnectionId: ENTITY_TEST_CONSTANTS.EXTERNAL_CONNECTION_ID, + entityId: ENTITY_TEST_CONSTANTS.ENTITY_ID, + isPrimarySource: true + }, + externalConnectionDetail: { + id: ENTITY_TEST_CONSTANTS.EXTERNAL_CONNECTION_ID, + connectionId: 'conn-123', + elementInstanceId: TEST_CONSTANTS.FOLDER_ID, + folderKey: 'test-folder', + connectorKey: 'salesforce', + connectorName: 'Salesforce', + connectionName: 'My Salesforce Connection' + } + } + ] + }); +}; + diff --git a/tests/utils/mocks/index.ts b/tests/utils/mocks/index.ts index 5bce619a..3b30ef7a 100644 --- a/tests/utils/mocks/index.ts +++ b/tests/utils/mocks/index.ts @@ -9,6 +9,7 @@ export * from './core'; // Service-specific mock utilities export * from './maestro'; export * from './tasks'; +export * from './entities'; // Re-export constants for convenience export * from '../constants'; \ No newline at end of file From 06d7b696d7029b50c300910c5e6a251707907102 Mon Sep 17 00:00:00 2001 From: Raina451 <133850046+Raina451@users.noreply.github.com> Date: Fri, 17 Oct 2025 19:59:07 +0530 Subject: [PATCH 5/7] Add case unit tests (#105) --- .../models/maestro/case-instances.test.ts | 358 +++++++++ tests/unit/models/maestro/index.ts | 3 +- .../services/maestro/case-instances.test.ts | 753 ++++++++++++++++++ tests/unit/services/maestro/cases.test.ts | 169 ++++ tests/unit/services/maestro/index.ts | 4 +- .../maestro/process-instances.test.ts | 32 +- tests/unit/services/maestro/processes.test.ts | 7 +- tests/utils/constants/common.ts | 1 + tests/utils/constants/maestro.ts | 109 +++ tests/utils/mocks/maestro.ts | 361 +++++++++ 10 files changed, 1776 insertions(+), 21 deletions(-) create mode 100644 tests/unit/models/maestro/case-instances.test.ts create mode 100644 tests/unit/services/maestro/case-instances.test.ts create mode 100644 tests/unit/services/maestro/cases.test.ts diff --git a/tests/unit/models/maestro/case-instances.test.ts b/tests/unit/models/maestro/case-instances.test.ts new file mode 100644 index 00000000..2f0fdd08 --- /dev/null +++ b/tests/unit/models/maestro/case-instances.test.ts @@ -0,0 +1,358 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + createCaseInstanceWithMethods, + CaseInstancesServiceModel +} from '../../../../src/models/maestro/case-instances.models'; +import { + MAESTRO_TEST_CONSTANTS, + TEST_CONSTANTS, + createMockOperationResponse, + createMockCaseInstance, + createMockCaseInstanceExecutionHistory, + createMockCaseStage +} from '../../../utils/mocks'; +import type { + CaseInstanceOperationOptions, +} from '../../../../src/models/maestro/case-instances.types'; + +// ===== TEST SUITE ===== +describe('Case Instance Models', () => { + let mockService: CaseInstancesServiceModel; + + beforeEach(() => { + // Create a mock service + mockService = { + getAll: vi.fn(), + getById: vi.fn(), + close: vi.fn(), + pause: vi.fn(), + resume: vi.fn(), + getExecutionHistory: vi.fn(), + getStages: vi.fn(), + getActionTasks: vi.fn() + } as any; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('bound methods on case instance', () => { + describe('caseInstance.close()', () => { + it('should call caseInstance.close with bound instanceId and folderKey', async () => { + const mockInstanceData = createMockCaseInstance(); + const instance = createCaseInstanceWithMethods(mockInstanceData, mockService); + + const mockResponse = createMockOperationResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + status: TEST_CONSTANTS.CANCELLED + }); + mockService.close = vi.fn().mockResolvedValue(mockResponse); + + await instance.close(); + + expect(mockService.close).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + undefined + ); + }); + + it('should call caseInstance.close with bound parameters and options', async () => { + const mockInstanceData = createMockCaseInstance(); + const instance = createCaseInstanceWithMethods(mockInstanceData, mockService); + + const mockResponse = createMockOperationResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + status: TEST_CONSTANTS.CANCELLED + }); + const options: CaseInstanceOperationOptions = { comment: MAESTRO_TEST_CONSTANTS.TEST_COMMENT }; + mockService.close = vi.fn().mockResolvedValue(mockResponse); + + await instance.close(options); + + expect(mockService.close).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + options + ); + }); + + it('should throw error if instanceId is undefined', async () => { + const mockInstanceData = createMockCaseInstance(); + const invalidInstanceData = { ...mockInstanceData, instanceId: undefined as any }; + const invalidInstance = createCaseInstanceWithMethods(invalidInstanceData, mockService); + + await expect(invalidInstance.close()).rejects.toThrow('Case instance ID is undefined'); + }); + + it('should throw error if folderKey is undefined', async () => { + const mockInstanceData = createMockCaseInstance(); + const invalidInstanceData = { ...mockInstanceData, folderKey: undefined as any }; + const invalidInstance = createCaseInstanceWithMethods(invalidInstanceData, mockService); + + await expect(invalidInstance.close()).rejects.toThrow('Case instance folder key is undefined'); + }); + }); + + describe('caseInstance.pause()', () => { + it('should call caseInstance.pause with bound instanceId and folderKey', async () => { + const mockInstanceData = createMockCaseInstance(); + const instance = createCaseInstanceWithMethods(mockInstanceData, mockService); + + const mockResponse = createMockOperationResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + status: MAESTRO_TEST_CONSTANTS.TASK_STATUS_PAUSED + }); + mockService.pause = vi.fn().mockResolvedValue(mockResponse); + + await instance.pause(); + + expect(mockService.pause).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + undefined + ); + }); + + it('should call caseInstance.pause with bound parameters and options', async () => { + const mockInstanceData = createMockCaseInstance(); + const instance = createCaseInstanceWithMethods(mockInstanceData, mockService); + + const mockResponse = createMockOperationResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + status: MAESTRO_TEST_CONSTANTS.TASK_STATUS_PAUSED + }); + const options: CaseInstanceOperationOptions = { comment: MAESTRO_TEST_CONSTANTS.TEST_COMMENT }; + mockService.pause = vi.fn().mockResolvedValue(mockResponse); + + await instance.pause(options); + + expect(mockService.pause).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + options + ); + }); + + it('should throw error if instanceId is undefined', async () => { + const mockInstanceData = createMockCaseInstance(); + const invalidInstanceData = { ...mockInstanceData, instanceId: undefined as any }; + const invalidInstance = createCaseInstanceWithMethods(invalidInstanceData, mockService); + + await expect(invalidInstance.pause()).rejects.toThrow('Case instance ID is undefined'); + }); + + it('should throw error if folderKey is undefined', async () => { + const mockInstanceData = createMockCaseInstance(); + const invalidInstanceData = { ...mockInstanceData, folderKey: undefined as any }; + const invalidInstance = createCaseInstanceWithMethods(invalidInstanceData, mockService); + + await expect(invalidInstance.pause()).rejects.toThrow('Case instance folder key is undefined'); + }); + }); + + describe('caseInstance.resume()', () => { + it('should call caseInstance.resume with bound instanceId and folderKey', async () => { + const mockInstanceData = createMockCaseInstance(); + const instance = createCaseInstanceWithMethods(mockInstanceData, mockService); + + const mockResponse = createMockOperationResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + status: TEST_CONSTANTS.RUNNING + }); + mockService.resume = vi.fn().mockResolvedValue(mockResponse); + + await instance.resume(); + + expect(mockService.resume).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + undefined + ); + }); + + it('should call caseInstance.resume with bound parameters and options', async () => { + const mockInstanceData = createMockCaseInstance(); + const instance = createCaseInstanceWithMethods(mockInstanceData, mockService); + + const mockResponse = createMockOperationResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + status: TEST_CONSTANTS.RUNNING + }); + const options: CaseInstanceOperationOptions = { comment: MAESTRO_TEST_CONSTANTS.TEST_COMMENT }; + mockService.resume = vi.fn().mockResolvedValue(mockResponse); + + await instance.resume(options); + + expect(mockService.resume).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + options + ); + }); + + it('should throw error if instanceId is undefined', async () => { + const mockInstanceData = createMockCaseInstance(); + const invalidInstanceData = { ...mockInstanceData, instanceId: undefined as any }; + const invalidInstance = createCaseInstanceWithMethods(invalidInstanceData, mockService); + + await expect(invalidInstance.resume()).rejects.toThrow('Case instance ID is undefined'); + }); + + it('should throw error if folderKey is undefined', async () => { + const mockInstanceData = createMockCaseInstance(); + const invalidInstanceData = { ...mockInstanceData, folderKey: undefined as any }; + const invalidInstance = createCaseInstanceWithMethods(invalidInstanceData, mockService); + + await expect(invalidInstance.resume()).rejects.toThrow('Case instance folder key is undefined'); + }); + }); + + describe('caseInstance.getExecutionHistory()', () => { + it('should call caseInstance.getExecutionHistory with bound instanceId and folderKey', async () => { + const mockInstanceData = createMockCaseInstance(); + const instance = createCaseInstanceWithMethods(mockInstanceData, mockService); + + const mockHistory = createMockCaseInstanceExecutionHistory(); + mockService.getExecutionHistory = vi.fn().mockResolvedValue(mockHistory); + + const result = await instance.getExecutionHistory(); + + expect(mockService.getExecutionHistory).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY + ); + expect(result).toEqual(mockHistory); + }); + + it('should throw error if instanceId is undefined', async () => { + const mockInstanceData = createMockCaseInstance(); + const invalidInstanceData = { ...mockInstanceData, instanceId: undefined as any }; + const invalidInstance = createCaseInstanceWithMethods(invalidInstanceData, mockService); + + await expect(invalidInstance.getExecutionHistory()).rejects.toThrow('Case instance ID is undefined'); + }); + + it('should throw error if folderKey is undefined', async () => { + const mockInstanceData = createMockCaseInstance(); + const invalidInstanceData = { ...mockInstanceData, folderKey: undefined as any }; + const invalidInstance = createCaseInstanceWithMethods(invalidInstanceData, mockService); + + await expect(invalidInstance.getExecutionHistory()).rejects.toThrow('Case instance folder key is undefined'); + }); + }); + + describe('caseInstance.getStages()', () => { + it('should call caseInstance.getStages with bound instanceId and folderKey', async () => { + const mockInstanceData = createMockCaseInstance(); + const instance = createCaseInstanceWithMethods(mockInstanceData, mockService); + + const mockStages = [createMockCaseStage()]; + mockService.getStages = vi.fn().mockResolvedValue(mockStages); + + const result = await instance.getStages(); + + expect(mockService.getStages).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + MAESTRO_TEST_CONSTANTS.FOLDER_KEY + ); + expect(result).toEqual(mockStages); + }); + + it('should throw error if instanceId is undefined', async () => { + const mockInstanceData = createMockCaseInstance(); + const invalidInstanceData = { ...mockInstanceData, instanceId: undefined as any }; + const invalidInstance = createCaseInstanceWithMethods(invalidInstanceData, mockService); + + await expect(invalidInstance.getStages()).rejects.toThrow('Case instance ID is undefined'); + }); + + it('should throw error if folderKey is undefined', async () => { + const mockInstanceData = createMockCaseInstance(); + const invalidInstanceData = { ...mockInstanceData, folderKey: undefined as any }; + const invalidInstance = createCaseInstanceWithMethods(invalidInstanceData, mockService); + + await expect(invalidInstance.getStages()).rejects.toThrow('Case instance folder key is undefined'); + }); + }); + + describe('caseInstance.getActionTasks()', () => { + it('should call caseInstance.getActionTasks with bound instanceId', async () => { + const mockInstanceData = createMockCaseInstance(); + const instance = createCaseInstanceWithMethods(mockInstanceData, mockService); + + const mockTasks = { + items: [], + totalCount: 0 + }; + mockService.getActionTasks = vi.fn().mockResolvedValue(mockTasks); + + const result = await instance.getActionTasks(); + + expect(mockService.getActionTasks).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + undefined + ); + expect(result).toEqual(mockTasks); + }); + + it('should call caseInstance.getActionTasks with bound parameters and options', async () => { + const mockInstanceData = createMockCaseInstance(); + const instance = createCaseInstanceWithMethods(mockInstanceData, mockService); + + const mockTasks = { + items: [], + totalCount: 0 + }; + const options = { pageSize: 10 }; + mockService.getActionTasks = vi.fn().mockResolvedValue(mockTasks); + + const result = await instance.getActionTasks(options); + + expect(mockService.getActionTasks).toHaveBeenCalledWith( + MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + options + ); + expect(result).toEqual(mockTasks); + }); + + it('should throw error if instanceId is undefined', async () => { + const mockInstanceData = createMockCaseInstance(); + const invalidInstanceData = { ...mockInstanceData, instanceId: undefined as any }; + const invalidInstance = createCaseInstanceWithMethods(invalidInstanceData, mockService); + + await expect(invalidInstance.getActionTasks()).rejects.toThrow('Case instance ID is undefined'); + }); + }); + }); + + describe('createCaseInstanceWithMethods', () => { + it('should create instance with all bound methods', () => { + const mockInstanceData = createMockCaseInstance(); + + const instance = createCaseInstanceWithMethods(mockInstanceData, mockService); + + expect(instance).toHaveProperty('instanceId', MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID); + expect(instance).toHaveProperty('folderKey', MAESTRO_TEST_CONSTANTS.FOLDER_KEY); + expect(typeof instance.close).toBe('function'); + expect(typeof instance.pause).toBe('function'); + expect(typeof instance.resume).toBe('function'); + expect(typeof instance.getExecutionHistory).toBe('function'); + expect(typeof instance.getStages).toBe('function'); + expect(typeof instance.getActionTasks).toBe('function'); + }); + + it('should preserve all original instance data', () => { + const mockInstanceData = createMockCaseInstance(); + + const instance = createCaseInstanceWithMethods(mockInstanceData, mockService); + + expect(instance.instanceId).toBe(mockInstanceData.instanceId); + expect(instance.packageId).toBe(mockInstanceData.packageId); + expect(instance.caseType).toBe(mockInstanceData.caseType); + expect(instance.caseTitle).toBe(mockInstanceData.caseTitle); + }); + + }); +}); \ No newline at end of file diff --git a/tests/unit/models/maestro/index.ts b/tests/unit/models/maestro/index.ts index f6395f98..8ad72ba7 100644 --- a/tests/unit/models/maestro/index.ts +++ b/tests/unit/models/maestro/index.ts @@ -2,4 +2,5 @@ * Maestro models test exports */ -export * from './process-instances.test'; \ No newline at end of file +export * from './process-instances.test'; +export * from './case-instances.test'; \ No newline at end of file diff --git a/tests/unit/services/maestro/case-instances.test.ts b/tests/unit/services/maestro/case-instances.test.ts new file mode 100644 index 00000000..84feac1e --- /dev/null +++ b/tests/unit/services/maestro/case-instances.test.ts @@ -0,0 +1,753 @@ +// ===== IMPORTS ===== +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { CaseInstancesService } from '../../../../src/services/maestro/case-instances'; +import { MAESTRO_ENDPOINTS } from '../../../../src/utils/constants/endpoints'; +import { ApiClient } from '../../../../src/core/http/api-client'; +import { FOLDER_KEY } from '../../../../src/utils/constants/headers'; +import { PaginationHelpers } from '../../../../src/utils/pagination/helpers'; +import { + MAESTRO_TEST_CONSTANTS, + TEST_CONSTANTS, + createMockCaseInstance, + createMockRawCaseInstance, + createMockCaseJsonResponse, + createMockCaseJsonWithStages, + createMockCaseJsonWithSections, + createMockCaseInstanceExecutionHistory, + createMockMaestroApiOperationResponse, + createMockActionTasksResponse, +} from '../../../utils/mocks'; +import { createMockBaseResponse } from '../../../utils/mocks/core'; +import { createServiceTestDependencies, createMockApiClient } from '../../../utils/setup'; +import type { + CaseInstanceGetAllWithPaginationOptions, + CaseInstanceOperationOptions, + CaseInstanceGetResponse +} from '../../../../src/models/maestro'; +import type { PaginatedResponse } from '../../../../src/utils/pagination/types'; +import { ProcessType } from '../../../../src/models/maestro/cases.internal-types'; + +// ===== MOCKING ===== +// Mock the dependencies +vi.mock('../../../../src/core/http/api-client'); + +// Use vi.hoisted to ensure mockPaginationHelpers is available during hoisting +const mocks = vi.hoisted(() => { + return import('../../../utils/mocks/core'); +}); + +vi.mock('../../../../src/utils/pagination/helpers', async () => (await mocks).mockPaginationHelpers); + +// ===== TEST SUITE ===== +describe('CaseInstancesService', () => { + let service: CaseInstancesService; + let mockApiClient: any; + + beforeEach(async () => { + // Create mock instances using centralized setup + const { config, executionContext, tokenManager } = createServiceTestDependencies(); + mockApiClient = createMockApiClient(); + + // Mock the ApiClient constructor + vi.mocked(ApiClient).mockImplementation(() => mockApiClient); + + // Reset pagination helpers mock before each test + vi.mocked(PaginationHelpers.getAll).mockReset(); + + service = new CaseInstancesService(config, executionContext, tokenManager); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('getAll', () => { + it('should return all case instances without pagination', async () => { + // Mock the pagination helper to return our test data + const mockResponse = { + items: [createMockCaseInstance()], + totalCount: 1 + }; + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const result = await service.getAll(); + + // Verify PaginationHelpers.getAll was called with correct parameters + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.any(Function), + transformFn: expect.any(Function), + pagination: expect.any(Object) + }), + expect.objectContaining({ + processType: ProcessType.CaseManagement + }) + ); + + expect(result).toEqual(mockResponse); + }); + + it('should return paginated case instances when pagination options provided', async () => { + const mockResponse = { + items: [createMockCaseInstance()], + totalCount: 100, + hasNextPage: true, + nextCursor: { value: TEST_CONSTANTS.NEXT_CURSOR }, + previousCursor: undefined, + currentPage: 1, + totalPages: 10, + supportsPageJump: true + }; + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const options: CaseInstanceGetAllWithPaginationOptions = { + pageSize: TEST_CONSTANTS.PAGE_SIZE + } as CaseInstanceGetAllWithPaginationOptions; + + const result = await service.getAll(options) as PaginatedResponse; + + // Verify PaginationHelpers.getAll was called with correct parameters + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.any(Function), + transformFn: expect.any(Function), + pagination: expect.any(Object) + }), + expect.objectContaining({ + pageSize: TEST_CONSTANTS.PAGE_SIZE, + processType: ProcessType.CaseManagement + }) + ); + + expect(result).toEqual(mockResponse); + expect(result.hasNextPage).toBe(true); + }); + + it('should handle filtering options', async () => { + // Mock the pagination helper to return our test data + const mockResponse = { + items: [], + totalCount: 0 + }; + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const options: CaseInstanceGetAllWithPaginationOptions = { + processKey: MAESTRO_TEST_CONSTANTS.CASE_PROCESS_KEY, + packageId: MAESTRO_TEST_CONSTANTS.CASE_PACKAGE_ID, + errorCode: MAESTRO_TEST_CONSTANTS.ERROR_CODE + }; + + await service.getAll(options); + + // Verify PaginationHelpers.getAll was called with correct parameters + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.any(Function), + transformFn: expect.any(Function), + pagination: expect.any(Object) + }), + expect.objectContaining({ + processKey: MAESTRO_TEST_CONSTANTS.CASE_PROCESS_KEY, + packageId: MAESTRO_TEST_CONSTANTS.CASE_PACKAGE_ID, + errorCode: MAESTRO_TEST_CONSTANTS.ERROR_CODE, + processType: ProcessType.CaseManagement + }) + ); + }); + + it('should handle API errors', async () => { + // Mock the pagination helper to throw an error + vi.mocked(PaginationHelpers.getAll).mockRejectedValue(new Error(TEST_CONSTANTS.ERROR_MESSAGE)); + + await expect(service.getAll()).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); + + describe('getById', () => { + it('should return case instance by ID with operation methods and transformed properties', async () => { + + const instanceId = MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + + // Mock raw API response with UTC time fields (before transformation) + const mockRawApiResponse = createMockRawCaseInstance(); + + const mockCaseJsonResponse = createMockCaseJsonResponse(); + + mockApiClient.get + .mockResolvedValueOnce(mockRawApiResponse) + .mockResolvedValueOnce(mockCaseJsonResponse); + + const result = await service.getById(instanceId, folderKey); + + // Verify API calls + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.INSTANCES.GET_BY_ID(instanceId), + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey + }) + } + ); + + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.CASES.GET_CASE_JSON(instanceId), + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey + }) + } + ); + + // Verify the result has the transformed properties (PascalCase -> camelCase) + expect(result).toHaveProperty('instanceId', MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID); + expect(result).toHaveProperty('packageKey', MAESTRO_TEST_CONSTANTS.PACKAGE_KEY); + expect(result).toHaveProperty('packageId', MAESTRO_TEST_CONSTANTS.CASE_PACKAGE_ID); + expect(result).toHaveProperty('latestRunStatus', TEST_CONSTANTS.RUNNING); + expect(result).toHaveProperty('startedTime', MAESTRO_TEST_CONSTANTS.START_TIME); // StartedTimeUtc -> startedTime + expect(result).not.toHaveProperty('startedTimeUtc'); // Original field should be removed + + // Verify case JSON fields are present (from enhancement) + expect(result).toHaveProperty('caseType', MAESTRO_TEST_CONSTANTS.CASE_JSON_RESPONSE.root.name); + expect(result).toHaveProperty('caseTitle', MAESTRO_TEST_CONSTANTS.CASE_JSON_RESPONSE.root.description); + expect(result).toHaveProperty('caseAppConfig'); + expect(result.caseAppConfig).toBeDefined(); + + // Verify operation methods are attached + expect(result).toHaveProperty('close'); + expect(result).toHaveProperty('pause'); + expect(typeof result.pause).toBe('function'); + expect(typeof result.resume).toBe('function'); + expect(result).toHaveProperty('getExecutionHistory'); + expect(result).toHaveProperty('getStages'); + expect(result).toHaveProperty('getActionTasks'); + }); + + it('should handle case JSON without caseAppConfig', async () => { + const instanceId = MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + + // Mock raw API response with UTC time fields (before transformation) + const mockRawApiResponse = createMockRawCaseInstance(); + + // Case JSON without caseAppConfig + const mockCaseJson = createMockBaseResponse({ + root: { + name: MAESTRO_TEST_CONSTANTS.CASE_TYPE, + description: MAESTRO_TEST_CONSTANTS.CASE_SUMMARY + // No caseAppConfig + } + }); + + mockApiClient.get + .mockResolvedValueOnce(mockRawApiResponse) + .mockResolvedValueOnce(mockCaseJson); + + const result = await service.getById(instanceId, folderKey); + + expect(result).toHaveProperty('caseType', MAESTRO_TEST_CONSTANTS.CASE_TYPE); + expect(result).toHaveProperty('caseTitle', MAESTRO_TEST_CONSTANTS.CASE_SUMMARY); + expect(result).not.toHaveProperty('caseAppConfig'); + }); + + it('should remove id field from overview items in caseAppConfig', async () => { + const instanceId = MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + + const mockRawApiResponse = createMockRawCaseInstance(); + const mockCaseJson = createMockCaseJsonWithSections(); + + mockApiClient.get + .mockResolvedValueOnce(mockRawApiResponse) + .mockResolvedValueOnce(mockCaseJson); + + const result = await service.getById(instanceId, folderKey); + + expect(result).toHaveProperty('caseAppConfig'); + expect(result.caseAppConfig).toBeDefined(); + expect(result.caseAppConfig).toHaveProperty('overview'); + + const overview = result.caseAppConfig?.overview; + expect(Array.isArray(overview)).toBe(true); + expect(overview).toHaveLength(2); + + // Verify id field is removed from overview items + overview?.forEach((item: any) => { + expect(item).not.toHaveProperty('id'); + expect(item).toHaveProperty('title'); + expect(item).toHaveProperty('details'); + }); + + // Verify constants are used correctly + if (overview) { + expect(overview[0].title).toBe(MAESTRO_TEST_CONSTANTS.CASE_APP_OVERVIEW_TITLE); + expect(overview[0].details).toBe(MAESTRO_TEST_CONSTANTS.CASE_APP_OVERVIEW_DETAILS); + } + }); + + it('should handle case JSON with empty description', async () => { + const instanceId = MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + + // Mock raw API response with UTC time fields (before transformation) + const mockRawApiResponse = createMockRawCaseInstance(); + + // Case JSON with empty description + const mockCaseJson = createMockBaseResponse({ + root: { + name: MAESTRO_TEST_CONSTANTS.CASE_TYPE, + description: '' + } + }); + + mockApiClient.get + .mockResolvedValueOnce(mockRawApiResponse) + .mockResolvedValueOnce(mockCaseJson); + + const result = await service.getById(instanceId, folderKey); + + expect(result).toHaveProperty('caseType', MAESTRO_TEST_CONSTANTS.CASE_TYPE); + expect(result).not.toHaveProperty('caseTitle'); // Should not have caseTitle when description is empty + expect(result).not.toHaveProperty('caseAppConfig'); + }); + + it('should handle case JSON fetch failure', async () => { + const instanceId = MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + + // Mock raw API response with UTC time fields (before transformation) + const mockRawApiResponse = createMockRawCaseInstance(); + + mockApiClient.get + .mockResolvedValueOnce(mockRawApiResponse) + .mockRejectedValueOnce(new Error('Case JSON fetch failed')); + + const result = await service.getById(instanceId, folderKey); + + expect(result).toHaveProperty('instanceId', instanceId); + expect(result).not.toHaveProperty('caseType'); + expect(result).not.toHaveProperty('caseTitle'); + }); + + it('should handle initial API error', async () => { + const error = new Error(TEST_CONSTANTS.ERROR_MESSAGE); + mockApiClient.get.mockRejectedValue(error); + + await expect(service.getById(MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)) + .rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + + it('should return instance without enhancement when folderKey is missing', async () => { + const instanceId = MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + + // Mock raw API response without folderKey (using override to set it to undefined) + const mockRawApiResponse = createMockRawCaseInstance({ folderKey: undefined }); + + mockApiClient.get.mockResolvedValueOnce(mockRawApiResponse); + + const result = await service.getById(instanceId, folderKey); + + // Verify only one API call was made (no case JSON call) + expect(mockApiClient.get).toHaveBeenCalledTimes(1); + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.INSTANCES.GET_BY_ID(instanceId), + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey + }) + } + ); + + // Verify the result doesn't have enhanced properties + expect(result).toHaveProperty('instanceId', MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID); + expect(result).not.toHaveProperty('caseType'); + expect(result).not.toHaveProperty('caseTitle'); + expect(result).not.toHaveProperty('caseAppConfig'); + // folderKey is undefined, which triggers the early return + expect(result.folderKey).toBeUndefined(); + }); + }); + + describe('close', () => { + it('should close case instance successfully', async () => { + + const instanceId = MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + const options: CaseInstanceOperationOptions = { + comment: MAESTRO_TEST_CONSTANTS.CANCEL_COMMENT + }; + const mockApiResponse = createMockMaestroApiOperationResponse({ + status: TEST_CONSTANTS.CANCELLED + }); + + mockApiClient.post.mockResolvedValue(mockApiResponse); + + const result = await service.close(instanceId, folderKey, options); + + expect(mockApiClient.post).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.INSTANCES.CANCEL(instanceId), + options, + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey + }) + } + ); + + expect(result.success).toBe(true); + expect(result.data).toEqual(mockApiResponse); + }); + + it('should close case instance without options', async () => { + + const instanceId = MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + const mockApiResponse = createMockMaestroApiOperationResponse({ + status: TEST_CONSTANTS.CANCELLED + }); + + mockApiClient.post.mockResolvedValue(mockApiResponse); + + const result = await service.close(instanceId, folderKey); + + expect(mockApiClient.post).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.INSTANCES.CANCEL(instanceId), + {}, + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey + }) + } + ); + + expect(result.success).toBe(true); + expect(result.data).toEqual(mockApiResponse); + }); + + it('should handle API errors', async () => { + + const error = new Error(TEST_CONSTANTS.ERROR_MESSAGE); + mockApiClient.post.mockRejectedValue(error); + + await expect(service.close(MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); + + describe('pause', () => { + it('should pause case instance successfully', async () => { + + const instanceId = MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + const options: CaseInstanceOperationOptions = { + comment: 'Pausing case instance' + }; + const mockApiResponse = createMockMaestroApiOperationResponse({ + status: MAESTRO_TEST_CONSTANTS.TASK_STATUS_PAUSED + }); + + mockApiClient.post.mockResolvedValue(mockApiResponse); + + const result = await service.pause(instanceId, folderKey, options); + + expect(mockApiClient.post).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.INSTANCES.PAUSE(instanceId), + options, + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey + }) + } + ); + + expect(result.success).toBe(true); + expect(result.data).toEqual(mockApiResponse); + }); + + it('should handle API errors', async () => { + + const error = new Error(TEST_CONSTANTS.ERROR_MESSAGE); + mockApiClient.post.mockRejectedValue(error); + + await expect(service.pause(MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); + + describe('resume', () => { + it('should resume case instance successfully', async () => { + + const instanceId = MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + const options: CaseInstanceOperationOptions = { + comment: 'Resuming case instance' + }; + const mockApiResponse = createMockMaestroApiOperationResponse({ + status: TEST_CONSTANTS.RUNNING + }); + + mockApiClient.post.mockResolvedValue(mockApiResponse); + + const result = await service.resume(instanceId, folderKey, options); + + expect(mockApiClient.post).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.INSTANCES.RESUME(instanceId), + options, + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey + }) + } + ); + + expect(result.success).toBe(true); + expect(result.data).toEqual(mockApiResponse); + }); + + it('should handle API errors', async () => { + + const error = new Error(TEST_CONSTANTS.ERROR_MESSAGE); + mockApiClient.post.mockRejectedValue(error); + + await expect(service.resume(MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); + + describe('getExecutionHistory', () => { + it('should return execution history for case instance with proper transformation', async () => { + + const instanceId = MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + + // Mock raw API response (before transformation) with UTC time fields + const mockRawApiResponse = createMockCaseInstanceExecutionHistory(); + + mockApiClient.get.mockResolvedValue(mockRawApiResponse); + + const result = await service.getExecutionHistory(instanceId, folderKey); + + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.CASES.GET_ELEMENT_EXECUTIONS(instanceId), + { + headers: expect.objectContaining({ + [FOLDER_KEY]: folderKey + }) + } + ); + + // Verify the result has the transformed properties + expect(result).toHaveProperty('instanceId', MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID); + expect(result).toHaveProperty('elementExecutions'); + expect(Array.isArray(result.elementExecutions)).toBe(true); + expect(result.elementExecutions).toHaveLength(1); + + // Verify transformation of element execution + const elementExecution = result.elementExecutions[0]; + expect(elementExecution).toHaveProperty('elementId', MAESTRO_TEST_CONSTANTS.CASE_TASK_ID); + expect(elementExecution).toHaveProperty('startedTime', MAESTRO_TEST_CONSTANTS.START_TIME); // Transformed field + expect(elementExecution).toHaveProperty('completedTime', MAESTRO_TEST_CONSTANTS.END_TIME); // Transformed field + expect(elementExecution).toHaveProperty('parentElementId', MAESTRO_TEST_CONSTANTS.CASE_STAGE_ID); + + // Verify that raw UTC fields are not present (transformed away) + expect(elementExecution).not.toHaveProperty('startedTimeUtc'); + + // Verify transformation of nested element runs + expect(elementExecution).toHaveProperty('elementRuns'); + expect(Array.isArray(elementExecution.elementRuns)).toBe(true); + expect(elementExecution.elementRuns).toHaveLength(1); + + const elementRun = elementExecution.elementRuns[0]; + expect(elementRun).toHaveProperty('elementRunId', MAESTRO_TEST_CONSTANTS.ELEMENT_RUN_ID); + expect(elementRun).toHaveProperty('startedTime', MAESTRO_TEST_CONSTANTS.START_TIME); // Transformed field + expect(elementRun).toHaveProperty('completedTime', MAESTRO_TEST_CONSTANTS.END_TIME); // Transformed field + expect(elementRun).toHaveProperty('parentElementRunId', null); + + // Verify that raw UTC fields are not present in nested runs (transformed away) + expect(elementRun).not.toHaveProperty('completedTimeUtc'); + }); + + it('should handle API errors', async () => { + + const error = new Error(TEST_CONSTANTS.ERROR_MESSAGE); + mockApiClient.get.mockRejectedValue(error); + + await expect(service.getExecutionHistory(MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); + + describe('getStages', () => { + const instanceId = MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID; + const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY; + + it('should return case stages with tasks, SLA, and execution data', async () => { + const mockExecutionHistory = createMockCaseInstanceExecutionHistory(); + const mockCaseJson = createMockCaseJsonWithStages(); // Default scenario with all features + + mockApiClient.get + .mockResolvedValueOnce(mockExecutionHistory) + .mockResolvedValueOnce(mockCaseJson); + + const result = await service.getStages(instanceId, folderKey); + + // Verify API calls + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.CASES.GET_ELEMENT_EXECUTIONS(instanceId), + { headers: expect.objectContaining({ [FOLDER_KEY]: folderKey }) } + ); + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.CASES.GET_CASE_JSON(instanceId), + { headers: expect.objectContaining({ [FOLDER_KEY]: folderKey }) } + ); + + // Verify stage data + expect(result).toHaveLength(1); + const stage = result[0]; + expect(stage.id).toBe(MAESTRO_TEST_CONSTANTS.CASE_STAGE_ID); + expect(stage.name).toBe(MAESTRO_TEST_CONSTANTS.CASE_STAGE_NAME); + + // Verify SLA data + expect(stage.sla).toBeDefined(); + expect(stage.sla).toHaveProperty('length', MAESTRO_TEST_CONSTANTS.SLA_COUNT_14_DAYS); + expect(stage.sla).toHaveProperty('duration', MAESTRO_TEST_CONSTANTS.SLA_DURATION_DAYS); + expect(stage.sla!.escalationRule).toHaveLength(1); + + // Verify task data with execution history + expect(stage.tasks).toHaveLength(1); + const task = stage.tasks[0][0]; + expect(task.id).toBe(MAESTRO_TEST_CONSTANTS.CASE_TASK_ID); + expect(task.name).toBe(MAESTRO_TEST_CONSTANTS.CASE_TASK_NAME); + expect(task.status).toBe(MAESTRO_TEST_CONSTANTS.TASK_STATUS_COMPLETED); + expect(task.startedTime).toBe(MAESTRO_TEST_CONSTANTS.START_TIME); + expect(task.completedTime).toBe(MAESTRO_TEST_CONSTANTS.END_TIME); + }); + + it('should handle edge cases gracefully', async () => { + // Test missing case JSON + mockApiClient.get + .mockResolvedValueOnce(createMockCaseInstanceExecutionHistory()) + .mockResolvedValueOnce(null); + + let result = await service.getStages(instanceId, folderKey); + expect(result).toEqual([]); + + mockApiClient.get.mockReset(); + + // Test empty nodes + mockApiClient.get + .mockResolvedValueOnce(createMockCaseInstanceExecutionHistory()) + .mockResolvedValueOnce(createMockCaseJsonWithStages('empty')); + + result = await service.getStages(instanceId, folderKey); + expect(result).toEqual([]); + + mockApiClient.get.mockReset(); + + // Test stage without tasks + mockApiClient.get + .mockResolvedValueOnce(createMockCaseInstanceExecutionHistory()) + .mockResolvedValueOnce(createMockCaseJsonWithStages('no-tasks')); + + result = await service.getStages(instanceId, folderKey); + expect(result).toHaveLength(1); + expect(result[0].tasks).toEqual([]); + + mockApiClient.get.mockReset(); + + // Test stage without SLA + mockApiClient.get + .mockResolvedValueOnce(createMockCaseInstanceExecutionHistory()) + .mockResolvedValueOnce(createMockCaseJsonWithStages('no-sla')); + + result = await service.getStages(instanceId, folderKey); + expect(result).toHaveLength(1); + expect(result[0].sla).toBeUndefined(); + + mockApiClient.get.mockReset(); + + // Test task name from bindings + mockApiClient.get + .mockResolvedValueOnce(createMockCaseInstanceExecutionHistory()) + .mockResolvedValueOnce(createMockCaseJsonWithStages('binding-task')); + + result = await service.getStages(instanceId, folderKey); + expect(result).toHaveLength(1); + expect(result[0].tasks[0][0].name).toBe(MAESTRO_TEST_CONSTANTS.BINDING_DEFAULT_RESOLVED); + }); + + it('should handle API errors gracefully', async () => { + mockApiClient.get + .mockRejectedValueOnce(new Error(TEST_CONSTANTS.ERROR_MESSAGE)) + .mockRejectedValueOnce(new Error(TEST_CONSTANTS.ERROR_MESSAGE)); + + const result = await service.getStages(instanceId, folderKey); + expect(result).toEqual([]); + + mockApiClient.get + .mockResolvedValueOnce(createMockCaseInstanceExecutionHistory()) + .mockRejectedValueOnce(new Error(TEST_CONSTANTS.ERROR_MESSAGE)); + + const result2 = await service.getStages(instanceId, folderKey); + expect(result2).toEqual([]); + }); + }); + + describe('getActionTasks', () => { + it('should return human in the loop tasks for case instance', async () => { + + const caseInstanceId = MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID; + const mockResponse = createMockActionTasksResponse(); + + const mockTaskService = { + getAll: vi.fn().mockResolvedValue(mockResponse) + }; + (service as any).taskService = mockTaskService; + + const result = await service.getActionTasks(caseInstanceId); + + expect(mockTaskService.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + filter: expect.stringContaining(caseInstanceId), + expand: expect.any(String) + }) + ); + + expect(result).toEqual(mockResponse); + }); + + it('should combine existing filter with case instance filter', async () => { + + const caseInstanceId = MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID; + const existingFilter = 'Status eq \'Pending\''; + const mockResponse = { + items: [], + totalCount: 0 + }; + + const mockTaskService = { + getAll: vi.fn().mockResolvedValue(mockResponse) + }; + (service as any).taskService = mockTaskService; + + const options = { + filter: existingFilter + }; + + const result = await service.getActionTasks(caseInstanceId, options); + + expect(mockTaskService.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + filter: expect.stringMatching(new RegExp(`.*${caseInstanceId}.*${existingFilter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*`)), + expand: expect.any(String) + }) + ); + + expect(result).toEqual(mockResponse); + }); + + }); +}); \ No newline at end of file diff --git a/tests/unit/services/maestro/cases.test.ts b/tests/unit/services/maestro/cases.test.ts new file mode 100644 index 00000000..2f08dad4 --- /dev/null +++ b/tests/unit/services/maestro/cases.test.ts @@ -0,0 +1,169 @@ +// ===== IMPORTS ===== +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { CasesService } from '../../../../src/services/maestro/cases'; +import { MAESTRO_ENDPOINTS } from '../../../../src/utils/constants/endpoints'; +import { ApiClient } from '../../../../src/core/http/api-client'; +import { + MAESTRO_TEST_CONSTANTS, + TEST_CONSTANTS, + createMockCase, + createMockCasesGetAllApiResponse, + createMockError +} from '../../../utils/mocks'; +import { createServiceTestDependencies, createMockApiClient } from '../../../utils/setup'; +import { ProcessType } from '../../../../src/models/maestro/cases.internal-types'; + +// ===== MOCKING ===== +// Mock the dependencies +vi.mock('../../../../src/core/http/api-client'); + +// ===== TEST SUITE ===== +describe('CasesService', () => { + let service: CasesService; + let mockApiClient: any; + + beforeEach(async () => { + // Create mock instances using centralized setup + const { config, executionContext, tokenManager } = createServiceTestDependencies(); + mockApiClient = createMockApiClient(); + + // Mock the ApiClient constructor + vi.mocked(ApiClient).mockImplementation(() => mockApiClient); + + service = new CasesService(config, executionContext, tokenManager); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('getAll', () => { + it('should return all case management processes with instance statistics', async () => { + + const mockApiResponse = createMockCasesGetAllApiResponse([ + createMockCase(), + createMockCase({ + processKey: MAESTRO_TEST_CONSTANTS.CASE_PROCESS_KEY, + packageId: MAESTRO_TEST_CONSTANTS.CASE_PACKAGE_ID, + name: MAESTRO_TEST_CONSTANTS.CASE_NAME + }) + ]); + mockApiClient.get.mockResolvedValue(mockApiResponse); + + + const result = await service.getAll(); + + + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.PROCESSES.GET_ALL, + { + params: { + processType: ProcessType.CaseManagement + } + } + ); + + expect(result).toHaveLength(2); + expect(result[0]).toMatchObject({ + processKey: MAESTRO_TEST_CONSTANTS.CASE_PROCESS_KEY, + packageId: MAESTRO_TEST_CONSTANTS.CASE_PACKAGE_ID, + name: MAESTRO_TEST_CONSTANTS.EXTRACTED_NAME_DEFAULT, // Service extracts name from packageId + folderKey: MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + folderName: TEST_CONSTANTS.FOLDER_NAME + }); + + expect(result[1]).toMatchObject({ + processKey: MAESTRO_TEST_CONSTANTS.CASE_PROCESS_KEY, + packageId: MAESTRO_TEST_CONSTANTS.CASE_PACKAGE_ID, + name: MAESTRO_TEST_CONSTANTS.EXTRACTED_NAME_DEFAULT, // Service extracts name from packageId + folderKey: MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + folderName: TEST_CONSTANTS.FOLDER_NAME + }); + }); + + it('should handle empty cases array', async () => { + + const mockApiResponse = { processes: [] }; + mockApiClient.get.mockResolvedValue(mockApiResponse); + + + const result = await service.getAll(); + + expect(result).toEqual([]); + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.PROCESSES.GET_ALL, + { + params: { + processType: ProcessType.CaseManagement + } + } + ); + }); + + it('should handle response without processes property', async () => { + + const mockApiResponse = { + // Response has data but no processes property + someOtherProperty: MAESTRO_TEST_CONSTANTS.OTHER_PROPERTY, + }; + mockApiClient.get.mockResolvedValue(mockApiResponse); + + const result = await service.getAll(); + + expect(mockApiClient.get).toHaveBeenCalledWith( + MAESTRO_ENDPOINTS.PROCESSES.GET_ALL, + { + params: { + processType: ProcessType.CaseManagement + } + } + ); + expect(result).toEqual([]); + expect(Array.isArray(result)).toBe(true); + expect(result).toHaveLength(0); + }); + + it('should handle API errors', async () => { + + const error = createMockError(TEST_CONSTANTS.ERROR_MESSAGE); + mockApiClient.get.mockRejectedValue(error); + + + await expect(service.getAll()).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + + it('should extract case name from packageId with CaseManagement prefix', async () => { + + const mockApiResponse = createMockCasesGetAllApiResponse([ + createMockCase({ + processKey: MAESTRO_TEST_CONSTANTS.CASE_PROCESS_KEY, + packageId: MAESTRO_TEST_CONSTANTS.PACKAGE_NAME_WITH_PREFIX + }) + ]); + + mockApiClient.get.mockResolvedValue(mockApiResponse); + + + const result = await service.getAll(); + + expect(result[0].name).toBe(MAESTRO_TEST_CONSTANTS.EXTRACTED_NAME_WITH_PREFIX); + }); + + it('should extract case name from packageId without CaseManagement prefix', async () => { + + const mockApiResponse = createMockCasesGetAllApiResponse([ + createMockCase({ + processKey: MAESTRO_TEST_CONSTANTS.CASE_PROCESS_KEY, + packageId: MAESTRO_TEST_CONSTANTS.PACKAGE_NAME_WITHOUT_PREFIX + }) + ]); + + mockApiClient.get.mockResolvedValue(mockApiResponse); + + + const result = await service.getAll(); + + expect(result[0].name).toBe(MAESTRO_TEST_CONSTANTS.EXTRACTED_NAME_WITHOUT_PREFIX); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/services/maestro/index.ts b/tests/unit/services/maestro/index.ts index 3e3b81ee..f7e99d7b 100644 --- a/tests/unit/services/maestro/index.ts +++ b/tests/unit/services/maestro/index.ts @@ -1,3 +1,5 @@ // Export all maestro service tests export * from './processes.test'; -export * from './process-instances.test'; \ No newline at end of file +export * from './process-instances.test'; +export * from './case-instances.test' +export * from './cases.test' \ No newline at end of file diff --git a/tests/unit/services/maestro/process-instances.test.ts b/tests/unit/services/maestro/process-instances.test.ts index 73bca3c3..28f4cb6f 100644 --- a/tests/unit/services/maestro/process-instances.test.ts +++ b/tests/unit/services/maestro/process-instances.test.ts @@ -97,7 +97,7 @@ describe('ProcessInstancesService', () => { const options: ProcessInstanceGetAllWithPaginationOptions = { pageSize: TEST_CONSTANTS.PAGE_SIZE, cursor: { - value: 'test-cursor-value' + value: TEST_CONSTANTS.CURSOR_VALUE } }; @@ -114,7 +114,7 @@ describe('ProcessInstancesService', () => { expect.objectContaining({ pageSize: TEST_CONSTANTS.PAGE_SIZE, cursor: expect.objectContaining({ - value: 'test-cursor-value' + value: TEST_CONSTANTS.CURSOR_VALUE }) }) ); @@ -197,11 +197,11 @@ describe('ProcessInstancesService', () => { it('should handle API errors', async () => { - const error = new Error('API Error'); + const error = new Error(TEST_CONSTANTS.ERROR_MESSAGE); mockApiClient.get.mockRejectedValue(error); - await expect(service.getById(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow('API Error'); + await expect(service.getById(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); }); @@ -230,11 +230,11 @@ describe('ProcessInstancesService', () => { it('should handle API errors', async () => { - const error = new Error('API Error'); + const error = new Error(TEST_CONSTANTS.ERROR_MESSAGE); mockApiClient.get.mockRejectedValue(error); - await expect(service.getExecutionHistory(MAESTRO_TEST_CONSTANTS.INSTANCE_ID)).rejects.toThrow('API Error'); + await expect(service.getExecutionHistory(MAESTRO_TEST_CONSTANTS.INSTANCE_ID)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); }); @@ -266,11 +266,11 @@ describe('ProcessInstancesService', () => { it('should handle API errors', async () => { - const error = new Error('API Error'); + const error = new Error(TEST_CONSTANTS.ERROR_MESSAGE); mockApiClient.get.mockRejectedValue(error); - await expect(service.getBpmn(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow('API Error'); + await expect(service.getBpmn(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); }); @@ -336,11 +336,11 @@ describe('ProcessInstancesService', () => { it('should handle API errors', async () => { - const error = new Error('API Error'); + const error = new Error(TEST_CONSTANTS.ERROR_MESSAGE); mockApiClient.post.mockRejectedValue(error); - await expect(service.cancel(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow('API Error'); + await expect(service.cancel(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); }); @@ -378,11 +378,11 @@ describe('ProcessInstancesService', () => { it('should handle API errors', async () => { - const error = new Error('API Error'); + const error = new Error(TEST_CONSTANTS.ERROR_MESSAGE); mockApiClient.post.mockRejectedValue(error); - await expect(service.pause(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow('API Error'); + await expect(service.pause(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); }); @@ -420,11 +420,11 @@ describe('ProcessInstancesService', () => { it('should handle API errors', async () => { - const error = new Error('API Error'); + const error = new Error(TEST_CONSTANTS.ERROR_MESSAGE); mockApiClient.post.mockRejectedValue(error); - await expect(service.resume(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow('API Error'); + await expect(service.resume(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); }); @@ -558,11 +558,11 @@ describe('ProcessInstancesService', () => { it('should handle API errors', async () => { - const error = new Error('API Error'); + const error = new Error(TEST_CONSTANTS.ERROR_MESSAGE); mockApiClient.get.mockRejectedValue(error); - await expect(service.getVariables(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow('API Error'); + await expect(service.getVariables(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); }); }); \ No newline at end of file diff --git a/tests/unit/services/maestro/processes.test.ts b/tests/unit/services/maestro/processes.test.ts index d1d300c2..ae01c9f2 100644 --- a/tests/unit/services/maestro/processes.test.ts +++ b/tests/unit/services/maestro/processes.test.ts @@ -7,7 +7,8 @@ import { MAESTRO_TEST_CONSTANTS, createMockProcess, createMockProcessesApiResponse, - createMockError + createMockError, + TEST_CONSTANTS } from '../../../utils/mocks'; import { createServiceTestDependencies, createMockApiClient } from '../../../utils/setup'; @@ -127,11 +128,11 @@ describe('MaestroProcessesService', () => { it('should handle API errors', async () => { - const error = createMockError('API Error'); + const error = createMockError(TEST_CONSTANTS.ERROR_MESSAGE); mockApiClient.get.mockRejectedValue(error); - await expect(service.getAll()).rejects.toThrow('API Error'); + await expect(service.getAll()).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); it('should set name field to packageId for each process', async () => { diff --git a/tests/utils/constants/common.ts b/tests/utils/constants/common.ts index 7e5061ed..df0bc271 100644 --- a/tests/utils/constants/common.ts +++ b/tests/utils/constants/common.ts @@ -13,6 +13,7 @@ export const TEST_CONSTANTS = { // Common values PAGE_SIZE: 10, + CURSOR_VALUE: 'test-cursor-value', ERROR_MESSAGE: 'API Error', // User Information diff --git a/tests/utils/constants/maestro.ts b/tests/utils/constants/maestro.ts index e927db3f..022f9bb6 100644 --- a/tests/utils/constants/maestro.ts +++ b/tests/utils/constants/maestro.ts @@ -49,4 +49,113 @@ export const MAESTRO_TEST_CONSTANTS = { VARIABLE_ID: 'var1', VARIABLE_NAME: 'Input Variable', VARIABLE_VALUE: 'test value', + + // Case-specific constants + CASE_PROCESS_KEY: 'CaseManagement.TestCase', + CASE_PACKAGE_ID: 'CaseManagement.TestCase', + CASE_NAME: 'Test Case', + CASE_INSTANCE_ID: 'case-instance-123', + CASE_TYPE: 'Test Case Type', + CASE_TITLE: 'Test Case Title', + CASE_SUMMARY: 'Test case summary', + CASE_STAGE_ID: 'stage-1', + CASE_STAGE_NAME: 'Test Stage', + CASE_TASK_ID: 'task-1', + CASE_TASK_NAME: 'Test Task', + CASE_ELEMENT_ID: 'element-1', + + // Task-related constants + TASK_TYPE_RPA: 'rpa', + TASK_STATUS_COMPLETED: 'Completed', + TASK_STATUS_PENDING: 'Pending', + TASK_STATUS_PAUSED: 'Paused', + TASK_TITLE_1: 'Task 1', + TASK_TITLE_2: 'Task 2', + + // SLA constants + SLA_LENGTH_24_HOURS: 24, + SLA_DURATION_HOURS: 'h', + SLA_DURATION_DAYS: 'd', + SLA_COUNT_14_DAYS: 14, + + // Binding constants + BINDING_ID: 'binding-1', + BINDING_DEFAULT_RESOLVED: 'Resolved Task Name', + + // Node type constants + NODE_TYPE_STAGE: 'Stage', + + // SLA Escalation constants + SLA_TRIGGER_TYPE_BREACHED: 'sla-breached', + SLA_ACTION_TYPE_NOTIFICATION: 'notification', + SLA_RECIPIENT_SCOPE_USER: 'User', + SLA_RECIPIENT_TARGET: '6917d827-6035-4e81-9d29-3ac9372c8a24', + + // Case App constants + CASE_APP_SECTION_ID_1: 'section-1', + CASE_APP_SECTION_ID_2: 'section-2', + CASE_APP_OVERVIEW_TITLE: 'Overview 1', + CASE_APP_OVERVIEW_DETAILS: 'Details 1', + CASE_APP_OVERVIEW_TITLE_2: 'Overview 2', + CASE_APP_OVERVIEW_DETAILS_2: 'Details 2', + + // Test operation constants + TEST_COMMENT: 'Test comment', + + // Package name constants for testing name extraction + PACKAGE_NAME_WITH_PREFIX: 'CaseManagement.Test-Case-Process', + PACKAGE_NAME_WITHOUT_PREFIX: 'RegularPackageName', + + // Expected extracted names + EXTRACTED_NAME_WITH_PREFIX: 'Test Case Process', + EXTRACTED_NAME_WITHOUT_PREFIX: 'RegularPackageName', + EXTRACTED_NAME_DEFAULT: 'TestCase', + + // Execution History constants + ELEMENT_RUN_ID: 'run-1', + EXTERNAL_LINK: 'https://test.uipath.com/task/123', + CASE_JSON_RESPONSE: { + root: { + name: 'Test Case Type', + description: 'Test Case Description', + caseAppEnabled: true, + caseAppConfig: { + caseAppUrl: 'https://test.com', + caseAppId: 'test-app-id' + }, + data: { + uipath: { + bindings: [ + { id: 'binding-1', name: 'Binding 1', default: 'Default Value 1' }, + { id: 'binding-2', name: 'Binding 2', default: 'Default Value 2' } + ] + } + }, + nodes: [ + { + id: 'stage-1', + type: 'stage', + data: { + label: 'Test Stage', + sla: { + length: 24, + duration: 'h', + escalationRule: [] + }, + tasks: [ + [ + { + id: 'task-1', + elementId: 'element-1', + displayName: 'Test Task', + type: 'external-agent', + data: { name: '=bindings.binding-1' } + } + ] + ] + } + } + ] + } + } } as const; \ No newline at end of file diff --git a/tests/utils/mocks/maestro.ts b/tests/utils/mocks/maestro.ts index 4ba9d546..af510611 100644 --- a/tests/utils/mocks/maestro.ts +++ b/tests/utils/mocks/maestro.ts @@ -168,3 +168,364 @@ export const createMockBpmnWithVariables = (overrides: Partial = {}) => { `; }; + +// Case-specific Mock Factories + +/** + * Creates a mock Case object + * @param overrides - Optional overrides for specific fields + * @returns Mock Case object + */ +export const createMockCase = (overrides: Partial = {}) => { + return createMockBaseResponse({ + processKey: MAESTRO_TEST_CONSTANTS.CASE_PROCESS_KEY, + packageId: MAESTRO_TEST_CONSTANTS.CASE_PACKAGE_ID, + name: MAESTRO_TEST_CONSTANTS.CASE_NAME, + folderKey: MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + folderName: TEST_CONSTANTS.FOLDER_NAME, + packageVersions: [MAESTRO_TEST_CONSTANTS.PACKAGE_VERSION], + versionCount: 1, + pendingCount: 0, + runningCount: 1, + completedCount: 0, + pausedCount: 0, + cancelledCount: 0, + faultedCount: 0, + retryingCount: 0, + resumingCount: 0, + pausingCount: 0, + cancelingCount: 0, + }, overrides); +}; + +/** + * Creates a mock Case Instance object + * @param overrides - Optional overrides for specific fields + * @returns Mock Case Instance object + */ +export const createMockCaseInstance = (overrides: Partial = {}) => { + return createMockBaseResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + packageKey: MAESTRO_TEST_CONSTANTS.PACKAGE_KEY, + packageId: MAESTRO_TEST_CONSTANTS.CASE_PACKAGE_ID, + packageVersion: MAESTRO_TEST_CONSTANTS.PACKAGE_VERSION, + latestRunId: MAESTRO_TEST_CONSTANTS.RUN_ID, + latestRunStatus: TEST_CONSTANTS.RUNNING, + processKey: MAESTRO_TEST_CONSTANTS.CASE_PROCESS_KEY, + folderKey: MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + userId: TEST_CONSTANTS.USER_ID, + instanceDisplayName: MAESTRO_TEST_CONSTANTS.INSTANCE_DISPLAY_NAME, + startedByUser: MAESTRO_TEST_CONSTANTS.STARTED_BY_USER, + source: MAESTRO_TEST_CONSTANTS.MANUAL_SOURCE, + creatorUserKey: MAESTRO_TEST_CONSTANTS.CREATOR_USER_KEY, + startedTime: new Date().toISOString(), + completedTime: null, + instanceRuns: [], + caseType: MAESTRO_TEST_CONSTANTS.CASE_TYPE, + caseTitle: MAESTRO_TEST_CONSTANTS.CASE_TITLE, + caseAppConfig: { + caseSummary: MAESTRO_TEST_CONSTANTS.CASE_SUMMARY, + overview: [ + { title: MAESTRO_TEST_CONSTANTS.CASE_APP_OVERVIEW_TITLE, details: MAESTRO_TEST_CONSTANTS.CASE_APP_OVERVIEW_DETAILS } + ] + } + }, overrides); +}; + +/** + * Creates a clean mock Case Instance object without case-related properties + * Used for testing enhancement scenarios where we need a base instance + * @param overrides - Optional overrides for specific fields + * @returns Clean mock Case Instance object without caseType, caseTitle, caseAppConfig + */ +export const createMockRawCaseInstance = (overrides: Partial = {}) => { + return createMockBaseResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + packageKey: MAESTRO_TEST_CONSTANTS.PACKAGE_KEY, + packageId: MAESTRO_TEST_CONSTANTS.CASE_PACKAGE_ID, + packageVersion: MAESTRO_TEST_CONSTANTS.PACKAGE_VERSION, + latestRunId: MAESTRO_TEST_CONSTANTS.RUN_ID, + latestRunStatus: TEST_CONSTANTS.RUNNING, + processKey: MAESTRO_TEST_CONSTANTS.CASE_PROCESS_KEY, + folderKey: MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + userId: TEST_CONSTANTS.USER_ID, + instanceDisplayName: MAESTRO_TEST_CONSTANTS.INSTANCE_DISPLAY_NAME, + startedByUser: MAESTRO_TEST_CONSTANTS.STARTED_BY_USER, + source: MAESTRO_TEST_CONSTANTS.MANUAL_SOURCE, + creatorUserKey: MAESTRO_TEST_CONSTANTS.CREATOR_USER_KEY, + startedTimeUtc: MAESTRO_TEST_CONSTANTS.START_TIME, + completedTimeUtc: null, + instanceRuns: [] + }, overrides); +}; + +/** + * Creates a mock Cases API response + * @param cases - Array of cases (optional) + * @returns Mock API response for cases + */ +export const createMockCasesGetAllApiResponse = (cases: any[] = []) => { + return createMockBaseResponse({ + processes: cases.length > 0 ? cases : [createMockCase()] + }); +}; + +/** + * Creates a mock Case JSON response with flexible configuration + * @param overrides - Optional overrides for specific fields + * @returns Mock Case JSON response object + */ +export const createMockCaseJsonResponse = (overrides: Partial = {}) => { + return createMockBaseResponse(MAESTRO_TEST_CONSTANTS.CASE_JSON_RESPONSE, overrides); +}; + +/** + * Creates a mock Case JSON response with caseAppConfig containing sections (for testing overview transformation) + * @param overrides - Optional overrides for specific fields + * @returns Mock Case JSON response with sections that transform to overview + */ +export const createMockCaseJsonWithSections = (overrides: Partial = {}) => { + return createMockBaseResponse({ + root: { + name: MAESTRO_TEST_CONSTANTS.CASE_TYPE, + description: MAESTRO_TEST_CONSTANTS.CASE_SUMMARY, + caseAppConfig: { + sections: [ + { + id: MAESTRO_TEST_CONSTANTS.CASE_APP_SECTION_ID_1, + title: MAESTRO_TEST_CONSTANTS.CASE_APP_OVERVIEW_TITLE, + details: MAESTRO_TEST_CONSTANTS.CASE_APP_OVERVIEW_DETAILS + }, + { + id: MAESTRO_TEST_CONSTANTS.CASE_APP_SECTION_ID_2, + title: MAESTRO_TEST_CONSTANTS.CASE_APP_OVERVIEW_TITLE_2, + details: MAESTRO_TEST_CONSTANTS.CASE_APP_OVERVIEW_DETAILS_2 + } + ] + } + } + }, overrides); +}; + +/** + * Creates a mock root object for Case JSON + * @param overrides - Optional overrides for specific fields + * @returns Mock root object with bindings + */ +export const createMockCaseJsonRoot = (overrides: Partial = {}) => { + return createMockBaseResponse({ + data: { + uipath: { + bindings: [{ + id: MAESTRO_TEST_CONSTANTS.BINDING_ID, + default: MAESTRO_TEST_CONSTANTS.BINDING_DEFAULT_RESOLVED + }] + } + } + }, overrides); +}; + +/** + * Creates a mock task object for Case JSON nodes + * @param overrides - Optional overrides for specific fields + * @returns Mock task object + */ +export const createMockCaseTask = (overrides: Partial = {}) => { + return createMockBaseResponse({ + id: MAESTRO_TEST_CONSTANTS.CASE_TASK_ID, + displayName: MAESTRO_TEST_CONSTANTS.CASE_TASK_NAME, + type: MAESTRO_TEST_CONSTANTS.TASK_TYPE_RPA, + elementId: MAESTRO_TEST_CONSTANTS.CASE_ELEMENT_ID + }, overrides); +}; + +/** + * Creates a mock SLA object for Case JSON nodes + * @param overrides - Optional overrides for specific fields + * @returns Mock SLA object with escalation rules + */ +export const createMockCaseSla = (overrides: Partial = {}) => { + return createMockBaseResponse({ + count: MAESTRO_TEST_CONSTANTS.SLA_COUNT_14_DAYS, + unit: MAESTRO_TEST_CONSTANTS.SLA_DURATION_DAYS, + escalationRule: [{ + triggerInfo: { type: MAESTRO_TEST_CONSTANTS.SLA_TRIGGER_TYPE_BREACHED }, + action: { + type: MAESTRO_TEST_CONSTANTS.SLA_ACTION_TYPE_NOTIFICATION, + recipients: [{ + scope: MAESTRO_TEST_CONSTANTS.SLA_RECIPIENT_SCOPE_USER, + target: MAESTRO_TEST_CONSTANTS.SLA_RECIPIENT_TARGET, + value: TEST_CONSTANTS.USER_EMAIL + }] + } + }] + }, overrides); +}; + +/** + * Creates a mock stage node for Case JSON + * @param overrides - Optional overrides for specific fields + * @returns Mock stage node with tasks and SLA + */ +export const createMockCaseStageNode = (overrides: Partial = {}) => { + return createMockBaseResponse({ + id: MAESTRO_TEST_CONSTANTS.CASE_STAGE_ID, + type: MAESTRO_TEST_CONSTANTS.NODE_TYPE_STAGE, + data: { + label: MAESTRO_TEST_CONSTANTS.CASE_STAGE_NAME, + sla: createMockCaseSla(), + tasks: [[createMockCaseTask()]] + } + }, overrides); +}; + +/** + * Creates a mock Case JSON response with stages and tasks + * @param scenario - Optional scenario to return different mock data + * @param overrides - Optional overrides for specific fields + * @returns Mock Case JSON response with stages configuration + */ +export const createMockCaseJsonWithStages = (scenario?: 'empty' | 'no-tasks' | 'no-sla' | 'binding-task', overrides: Partial = {}) => { + switch (scenario) { + case 'empty': + return createMockBaseResponse({ nodes: [] }, overrides); + + case 'no-tasks': + return createMockBaseResponse({ + nodes: [createMockCaseStageNode({ + data: { label: MAESTRO_TEST_CONSTANTS.CASE_STAGE_NAME } + })] + }, overrides); + + case 'no-sla': + return createMockBaseResponse({ + nodes: [createMockCaseStageNode({ + data: { + label: MAESTRO_TEST_CONSTANTS.CASE_STAGE_NAME, + tasks: [[createMockCaseTask()]] + } + })] + }, overrides); + + case 'binding-task': + return createMockBaseResponse({ + nodes: [createMockCaseStageNode({ + data: { + label: MAESTRO_TEST_CONSTANTS.CASE_STAGE_NAME, + tasks: [[createMockCaseTask({ + displayName: undefined, + data: { name: `=bindings.${MAESTRO_TEST_CONSTANTS.BINDING_ID}` } + })]] + } + })], + root: createMockCaseJsonRoot() + }, overrides); + + default: + // Default case with all features (SLA, tasks, bindings) + return createMockBaseResponse({ + nodes: [createMockCaseStageNode()] + }, overrides); + } +}; + + +/** + * Creates a mock Case Instance Execution History response + * @param overrides - Optional overrides for specific fields + * @returns Mock Case Instance Execution History response object + */ +/** + * Creates a mock Case Instance Execution History response with raw API fields (before transformation) + * The service will transform startedTimeUtc/completedTimeUtc to startedTime/completedTime + * @param overrides - Optional overrides for specific fields + * @returns Mock Case Instance Execution History response object with raw API fields + */ +export const createMockCaseInstanceExecutionHistory = (overrides: Partial = {}) => { + return createMockBaseResponse({ + instanceId: MAESTRO_TEST_CONSTANTS.CASE_INSTANCE_ID, + elementExecutions: [ + { + elementId: MAESTRO_TEST_CONSTANTS.CASE_TASK_ID, + elementName: MAESTRO_TEST_CONSTANTS.CASE_TASK_NAME, + status: MAESTRO_TEST_CONSTANTS.TASK_STATUS_COMPLETED, + startedTimeUtc: MAESTRO_TEST_CONSTANTS.START_TIME, // Raw API field + completedTimeUtc: MAESTRO_TEST_CONSTANTS.END_TIME, // Raw API field + parentElementId: MAESTRO_TEST_CONSTANTS.CASE_STAGE_ID, + processKey: MAESTRO_TEST_CONSTANTS.CASE_PROCESS_KEY, + externalLink: MAESTRO_TEST_CONSTANTS.EXTERNAL_LINK, + elementRuns: [ + { + elementRunId: MAESTRO_TEST_CONSTANTS.ELEMENT_RUN_ID, + status: MAESTRO_TEST_CONSTANTS.TASK_STATUS_COMPLETED, + startedTimeUtc: MAESTRO_TEST_CONSTANTS.START_TIME, // Raw API field + completedTimeUtc: MAESTRO_TEST_CONSTANTS.END_TIME, // Raw API field + parentElementRunId: null + } + ] + } + ] + }, overrides); +}; + +/** + * Creates a mock Case Stage response + * @param overrides - Optional overrides for specific fields + * @returns Mock Case Stage response object + */ +export const createMockCaseStage = (overrides: Partial = {}) => { + return createMockBaseResponse({ + id: MAESTRO_TEST_CONSTANTS.CASE_STAGE_ID, + name: MAESTRO_TEST_CONSTANTS.CASE_STAGE_NAME, + status: TEST_CONSTANTS.RUNNING, + sla: { + length: MAESTRO_TEST_CONSTANTS.SLA_LENGTH_24_HOURS, + duration: MAESTRO_TEST_CONSTANTS.SLA_DURATION_HOURS, + escalationRule: [] + }, + tasks: [ + [ + { + id: MAESTRO_TEST_CONSTANTS.CASE_TASK_ID, + name: MAESTRO_TEST_CONSTANTS.CASE_TASK_NAME, + completedTime: MAESTRO_TEST_CONSTANTS.END_TIME, + startedTime: MAESTRO_TEST_CONSTANTS.START_TIME, + status: MAESTRO_TEST_CONSTANTS.TASK_STATUS_COMPLETED, + type: MAESTRO_TEST_CONSTANTS.TASK_TYPE_RPA + } + ] + ] + }, overrides); +}; + +/** + * Creates a mock Action Task object (for Human-in-the-Loop tasks) + * @param overrides - Optional overrides for specific fields + * @returns Mock Action Task object with id, title, and status + */ +export const createMockActionTask = (overrides: Partial = {}) => { + return createMockBaseResponse({ + id: 1, + title: MAESTRO_TEST_CONSTANTS.TASK_TITLE_1, + status: MAESTRO_TEST_CONSTANTS.TASK_STATUS_PENDING + }, overrides); +}; + +/** + * Creates a mock Action Tasks paginated response + * @param tasks - Array of tasks (optional) + * @returns Mock paginated response with items and totalCount + */ +export const createMockActionTasksResponse = (tasks: any[] = []) => { + const defaultTasks = [ + createMockActionTask(), + createMockActionTask({ id: 2, title: MAESTRO_TEST_CONSTANTS.TASK_TITLE_2, status: MAESTRO_TEST_CONSTANTS.TASK_STATUS_COMPLETED }) + ]; + + const taskItems = tasks.length > 0 ? tasks : defaultTasks; + + return createMockBaseResponse({ + items: taskItems, + totalCount: taskItems.length + }); +}; From 68db911bb4b51fa6bd568f337303b43cc87ef838 Mon Sep 17 00:00:00 2001 From: swati354 <103816801+swati354@users.noreply.github.com> Date: Wed, 29 Oct 2025 07:46:21 +0530 Subject: [PATCH 6/7] Add unit tests for Asset Service (#107) --- .../unit/services/orchestrator/assets.test.ts | 232 ++++++++++++++++++ tests/unit/services/orchestrator/index.js | 1 + tests/utils/constants/assets.ts | 37 +++ tests/utils/constants/index.ts | 1 + tests/utils/mocks/assets.ts | 120 +++++++++ tests/utils/mocks/index.ts | 1 + 6 files changed, 392 insertions(+) create mode 100644 tests/unit/services/orchestrator/assets.test.ts create mode 100644 tests/unit/services/orchestrator/index.js create mode 100644 tests/utils/constants/assets.ts create mode 100644 tests/utils/mocks/assets.ts diff --git a/tests/unit/services/orchestrator/assets.test.ts b/tests/unit/services/orchestrator/assets.test.ts new file mode 100644 index 00000000..9303e749 --- /dev/null +++ b/tests/unit/services/orchestrator/assets.test.ts @@ -0,0 +1,232 @@ +// ===== IMPORTS ===== +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { AssetService } from '../../../../src/services/orchestrator/assets'; +import { ApiClient } from '../../../../src/core/http/api-client'; +import { PaginationHelpers } from '../../../../src/utils/pagination/helpers'; +import { + createMockRawAsset, + createMockTransformedAssetCollection +} from '../../../utils/mocks/assets'; +import { createServiceTestDependencies, createMockApiClient } from '../../../utils/setup'; +import { createMockError } from '../../../utils/mocks/core'; +import { + AssetGetAllOptions, + AssetGetByIdOptions, + AssetValueType, + AssetValueScope, + AssetGetResponse +} from '../../../../src/models/orchestrator/assets.types'; +import { PaginatedResponse } from '../../../../src/utils/pagination'; +import { ASSET_TEST_CONSTANTS } from '../../../utils/constants/assets'; +import { TEST_CONSTANTS } from '../../../utils/constants/common'; +import { ASSET_ENDPOINTS } from '../../../../src/utils/constants/endpoints'; +import { FOLDER_ID } from '../../../../src/utils/constants/headers'; + +// ===== MOCKING ===== +// Mock the dependencies +vi.mock('../../../../src/core/http/api-client'); + +// Import mock objects using vi.hoisted() - this ensures they're available before vi.mock() calls +const mocks = vi.hoisted(() => { + // Import/re-export the mock utilities from core + return import('../../../utils/mocks/core'); +}); + +// Setup mocks at module level +// NOTE: We do NOT mock transformData +vi.mock('../../../../src/utils/pagination/helpers', async () => (await mocks).mockPaginationHelpers); + +// ===== TEST SUITE ===== +describe('AssetService Unit Tests', () => { + let assetService: AssetService; + let mockApiClient: any; + + beforeEach(() => { + // Create mock instances using centralized setup + const { config, executionContext, tokenManager } = createServiceTestDependencies(); + mockApiClient = createMockApiClient(); + + // Mock the ApiClient constructor + vi.mocked(ApiClient).mockImplementation(() => mockApiClient); + + // Reset pagination helpers mock before each test + vi.mocked(PaginationHelpers.getAll).mockReset(); + + assetService = new AssetService(config, executionContext, tokenManager); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('getById', () => { + it('should get asset by ID successfully with all fields mapped correctly', async () => { + const mockAsset = createMockRawAsset(); + + mockApiClient.get.mockResolvedValue(mockAsset); + + const result = await assetService.getById( + ASSET_TEST_CONSTANTS.ASSET_ID, + TEST_CONSTANTS.FOLDER_ID + ); + + // Verify the result + expect(result).toBeDefined(); + expect(result.id).toBe(ASSET_TEST_CONSTANTS.ASSET_ID); + expect(result.name).toBe(ASSET_TEST_CONSTANTS.ASSET_NAME); + expect(result.key).toBe(ASSET_TEST_CONSTANTS.ASSET_KEY); + expect(result.valueType).toBe(AssetValueType.DBConnectionString,); + expect(result.valueScope).toBe(AssetValueScope.Global,); + + // Verify the API call has correct endpoint and headers + expect(mockApiClient.get).toHaveBeenCalledWith( + ASSET_ENDPOINTS.GET_BY_ID(ASSET_TEST_CONSTANTS.ASSET_ID), + expect.objectContaining({ + headers: expect.objectContaining({ + [FOLDER_ID]: TEST_CONSTANTS.FOLDER_ID.toString() + }) + }) + ); + + // Verify field transformations + // CreationTime -> createdTime + expect(result.createdTime).toBe(ASSET_TEST_CONSTANTS.CREATED_TIME); + expect((result as any).CreationTime).toBeUndefined(); // Original field should be removed + + // LastModificationTime -> lastModifiedTime + expect(result.lastModifiedTime).toBe(ASSET_TEST_CONSTANTS.LAST_MODIFIED_TIME); + expect((result as any).LastModificationTime).toBeUndefined(); // Original field should be removed + }); + + it('should get asset with options successfully', async () => { + const mockAsset = createMockRawAsset(); + mockApiClient.get.mockResolvedValue(mockAsset); + + const options: AssetGetByIdOptions = { + expand: ASSET_TEST_CONSTANTS.ODATA_EXPAND_KEY_VALUE_LIST, + select: ASSET_TEST_CONSTANTS.ODATA_SELECT_FIELDS + }; + + const result = await assetService.getById( + ASSET_TEST_CONSTANTS.ASSET_ID, + TEST_CONSTANTS.FOLDER_ID, + options + ); + + // Verify the result + expect(result).toBeDefined(); + expect(result.id).toBe(ASSET_TEST_CONSTANTS.ASSET_ID); + expect(result.name).toBe(ASSET_TEST_CONSTANTS.ASSET_NAME); + expect(result.key).toBe(ASSET_TEST_CONSTANTS.ASSET_KEY); + + // Verify API call has options with OData prefix + expect(mockApiClient.get).toHaveBeenCalledWith( + ASSET_ENDPOINTS.GET_BY_ID(ASSET_TEST_CONSTANTS.ASSET_ID), + expect.objectContaining({ + params: expect.objectContaining({ + '$expand': ASSET_TEST_CONSTANTS.ODATA_EXPAND_KEY_VALUE_LIST, + '$select': ASSET_TEST_CONSTANTS.ODATA_SELECT_FIELDS + }) + }) + ); + }); + + it('should handle API errors', async () => { + const error = createMockError(ASSET_TEST_CONSTANTS.ERROR_ASSET_NOT_FOUND); + mockApiClient.get.mockRejectedValue(error); + + await expect(assetService.getById( + ASSET_TEST_CONSTANTS.ASSET_ID, + TEST_CONSTANTS.FOLDER_ID + )).rejects.toThrow(ASSET_TEST_CONSTANTS.ERROR_ASSET_NOT_FOUND); + }); + }); + + describe('getAll', () => { + it('should return all assets without pagination options', async () => { + const mockResponse = createMockTransformedAssetCollection(); + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const result = await assetService.getAll(); + + // Verify PaginationHelpers.getAll was called + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.toSatisfy((fn: Function) => fn() === ASSET_ENDPOINTS.GET_ALL), + transformFn: expect.any(Function), + pagination: expect.any(Object) + }), + undefined + ); + + expect(result).toEqual(mockResponse); + }); + + it('should return assets filtered by folder ID', async () => { + const mockResponse = createMockTransformedAssetCollection(); + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const options: AssetGetAllOptions = { + folderId: TEST_CONSTANTS.FOLDER_ID + }; + + const result = await assetService.getAll(options); + + // Verify PaginationHelpers.getAll was called with folder options + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.toSatisfy((fn: Function) => fn(TEST_CONSTANTS.FOLDER_ID) === ASSET_ENDPOINTS.GET_BY_FOLDER), + transformFn: expect.any(Function), + pagination: expect.any(Object) + }), + expect.objectContaining({ + folderId: TEST_CONSTANTS.FOLDER_ID + }) + ); + + expect(result).toEqual(mockResponse); + }); + + it('should return paginated assets when pagination options provided', async () => { + const mockResponse = createMockTransformedAssetCollection(100, { + totalCount: 100, + hasNextPage: true, + nextCursor: TEST_CONSTANTS.NEXT_CURSOR, + previousCursor: null, + currentPage: 1, + totalPages: 10 + }); + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const options: AssetGetAllOptions = { + pageSize: TEST_CONSTANTS.PAGE_SIZE + }; + + const result = await assetService.getAll(options) as PaginatedResponse; + + // Verify PaginationHelpers.getAll was called with pagination options + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + pageSize: TEST_CONSTANTS.PAGE_SIZE + }) + ); + + expect(result).toEqual(mockResponse); + expect(result.hasNextPage).toBe(true); + expect(result.nextCursor).toBe(TEST_CONSTANTS.NEXT_CURSOR); + }); + + it('should handle API errors', async () => { + const error = createMockError(TEST_CONSTANTS.ERROR_MESSAGE); + vi.mocked(PaginationHelpers.getAll).mockRejectedValue(error); + + await expect(assetService.getAll()).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); +}); diff --git a/tests/unit/services/orchestrator/index.js b/tests/unit/services/orchestrator/index.js new file mode 100644 index 00000000..dcadd9a9 --- /dev/null +++ b/tests/unit/services/orchestrator/index.js @@ -0,0 +1 @@ +export * from './assets.test'; \ No newline at end of file diff --git a/tests/utils/constants/assets.ts b/tests/utils/constants/assets.ts new file mode 100644 index 00000000..d14531cd --- /dev/null +++ b/tests/utils/constants/assets.ts @@ -0,0 +1,37 @@ +/** + * Asset service test constants + * Asset-specific constants only + */ + +export const ASSET_TEST_CONSTANTS = { + // Asset IDs + ASSET_ID: 123, + + // Asset Metadata + ASSET_NAME: 'DatabaseConnection', + ASSET_KEY: '12345678-1234-1234-1234-123456789abc', + ASSET_DESCRIPTION: 'Database connection string for production', + + // Asset Values + ASSET_VALUE: 'Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;', + + // Timestamps + CREATED_TIME: '2023-10-15T10:00:00Z', + LAST_MODIFIED_TIME: '2023-10-20T15:30:00Z', + + // User IDs + LAST_MODIFIER_USER_ID: 102, + + // Key-Value List Items + KEY_VALUE_ITEM_1_KEY: 'environment', + KEY_VALUE_ITEM_1_VALUE: 'production', + KEY_VALUE_ITEM_2_KEY: 'region', + KEY_VALUE_ITEM_2_VALUE: 'us-east-1', + + // Error Messages + ERROR_ASSET_NOT_FOUND: 'Asset not found', + + // OData Parameters + ODATA_EXPAND_KEY_VALUE_LIST: 'keyValueList', + ODATA_SELECT_FIELDS: 'id,name,value', +} as const; diff --git a/tests/utils/constants/index.ts b/tests/utils/constants/index.ts index cca8f3ba..cbb66837 100644 --- a/tests/utils/constants/index.ts +++ b/tests/utils/constants/index.ts @@ -6,4 +6,5 @@ export * from './common'; export * from './maestro'; export * from './tasks'; export * from './entities'; +export * from './assets'; diff --git a/tests/utils/mocks/assets.ts b/tests/utils/mocks/assets.ts new file mode 100644 index 00000000..350d032f --- /dev/null +++ b/tests/utils/mocks/assets.ts @@ -0,0 +1,120 @@ +/** + * Asset service mock utilities - Asset-specific mocks only + * Uses generic utilities from core.ts for base functionality + */ +import { AssetValueScope, AssetValueType, AssetGetResponse } from '../../../src/models/orchestrator/assets.types'; +import { createMockBaseResponse, createMockCollection } from './core'; +import { ASSET_TEST_CONSTANTS } from '../constants/assets'; +import { TEST_CONSTANTS } from '../constants/common'; + +/** + * Creates a mock asset with RAW API format (before transformation) + * Uses PascalCase field names and raw API timestamp fields that need transformation + * + * @param overrides - Optional overrides for specific fields + * @returns Raw asset data as it comes from the API (before transformation) + */ +export const createMockRawAsset = (overrides: Partial = {}): any => { + return createMockBaseResponse({ + Id: ASSET_TEST_CONSTANTS.ASSET_ID, + Name: ASSET_TEST_CONSTANTS.ASSET_NAME, + Key: ASSET_TEST_CONSTANTS.ASSET_KEY, + Description: ASSET_TEST_CONSTANTS.ASSET_DESCRIPTION, + ValueScope: AssetValueScope.Global, + ValueType: AssetValueType.DBConnectionString, + Value: ASSET_TEST_CONSTANTS.ASSET_VALUE, + KeyValueList: [ + { + Key: ASSET_TEST_CONSTANTS.KEY_VALUE_ITEM_1_KEY, + Value: ASSET_TEST_CONSTANTS.KEY_VALUE_ITEM_1_VALUE + }, + { + Key: ASSET_TEST_CONSTANTS.KEY_VALUE_ITEM_2_KEY, + Value: ASSET_TEST_CONSTANTS.KEY_VALUE_ITEM_2_VALUE + } + ], + CredentialStoreId: null, + HasDefaultValue: true, + CanBeDeleted: true, + FoldersCount: 1, + // Using raw API field names that should be transformed + CreationTime: ASSET_TEST_CONSTANTS.CREATED_TIME, + CreatorUserId: TEST_CONSTANTS.USER_ID, + LastModificationTime: ASSET_TEST_CONSTANTS.LAST_MODIFIED_TIME, + LastModifierUserId: ASSET_TEST_CONSTANTS.LAST_MODIFIER_USER_ID, + }, overrides); +}; + +/** + * Creates a basic asset object with TRANSFORMED data (not raw API format) + * + * @param overrides - Optional overrides for specific fields + * @returns Asset with transformed field names (camelCase) + */ +export const createBasicAsset = (overrides: Partial = {}): AssetGetResponse => { + return createMockBaseResponse({ + id: ASSET_TEST_CONSTANTS.ASSET_ID, + name: ASSET_TEST_CONSTANTS.ASSET_NAME, + key: ASSET_TEST_CONSTANTS.ASSET_KEY, + description: ASSET_TEST_CONSTANTS.ASSET_DESCRIPTION, + valueScope: AssetValueScope.Global, + valueType: AssetValueType.DBConnectionString, + value: ASSET_TEST_CONSTANTS.ASSET_VALUE, + keyValueList: [ + { + key: ASSET_TEST_CONSTANTS.KEY_VALUE_ITEM_1_KEY, + value: ASSET_TEST_CONSTANTS.KEY_VALUE_ITEM_1_VALUE + }, + { + key: ASSET_TEST_CONSTANTS.KEY_VALUE_ITEM_2_KEY, + value: ASSET_TEST_CONSTANTS.KEY_VALUE_ITEM_2_VALUE + } + ], + credentialStoreId: null, + hasDefaultValue: true, + canBeDeleted: true, + foldersCount: 1, + // Using transformed field names (camelCase) + createdTime: ASSET_TEST_CONSTANTS.CREATED_TIME, + creatorUserId: TEST_CONSTANTS.USER_ID, + lastModifiedTime: ASSET_TEST_CONSTANTS.LAST_MODIFIED_TIME, + lastModifierUserId: ASSET_TEST_CONSTANTS.LAST_MODIFIER_USER_ID, + }, overrides); +}; + + +/** + * Creates a mock transformed asset collection response as returned by PaginationHelpers.getAll + * + * @param count - Number of assets to include (defaults to 1) + * @param options - Additional options like totalCount, pagination details + * @returns Mock transformed asset collection with items array + */ +export const createMockTransformedAssetCollection = ( + count: number = 1, + options?: { + totalCount?: number; + hasNextPage?: boolean; + nextCursor?: string; + previousCursor?: string | null; + currentPage?: number; + totalPages?: number; + } +): any => { + const items = createMockCollection(count, (index) => createBasicAsset({ + id: ASSET_TEST_CONSTANTS.ASSET_ID + index, + name: `${ASSET_TEST_CONSTANTS.ASSET_NAME}${index + 1}`, + // Generate unique GUIDs for each asset + key: `${index}-${ASSET_TEST_CONSTANTS.ASSET_KEY}` + })); + + return createMockBaseResponse({ + items, + totalCount: options?.totalCount || count, + ...(options?.hasNextPage !== undefined && { hasNextPage: options.hasNextPage }), + ...(options?.nextCursor && { nextCursor: options.nextCursor }), + ...(options?.previousCursor !== undefined && { previousCursor: options.previousCursor }), + ...(options?.currentPage !== undefined && { currentPage: options.currentPage }), + ...(options?.totalPages !== undefined && { totalPages: options.totalPages }) + }); +}; \ No newline at end of file diff --git a/tests/utils/mocks/index.ts b/tests/utils/mocks/index.ts index 3b30ef7a..e2f084bc 100644 --- a/tests/utils/mocks/index.ts +++ b/tests/utils/mocks/index.ts @@ -10,6 +10,7 @@ export * from './core'; export * from './maestro'; export * from './tasks'; export * from './entities'; +export * from './assets'; // Re-export constants for convenience export * from '../constants'; \ No newline at end of file From b7cd59f3a9cf77a5f308dd1ac3a205851baecb62 Mon Sep 17 00:00:00 2001 From: Raina451 Date: Wed, 29 Oct 2025 11:22:57 +0530 Subject: [PATCH 7/7] update vitest config --- vitest.config.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/vitest.config.ts b/vitest.config.ts index 4c4bc066..5cd4a608 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -18,6 +18,38 @@ export default defineConfig({ '**/*.d.ts', '**/*.config.*', '**/index.ts', + + // Type definitions and constants + '**/*.types.ts', + '**/types.ts', + '**/*.internal-types.ts', + '**/internal-types.ts', + '**/*.constants.ts', + '**/constants/**', + '**/constants.ts', + + // Error class definitions (exclude all except parser and error-factory which have logic) + 'src/core/errors/*.ts', + '!src/core/errors/parser.ts', + '!src/core/errors/error-factory.ts', + + // Pure interface model files (no logic, just TypeScript interfaces) + 'src/models/maestro/cases.models.ts', + 'src/models/maestro/processes.models.ts', + 'src/models/orchestrator/assets.models.ts', + 'src/models/orchestrator/buckets.models.ts', + 'src/models/orchestrator/processes.models.ts', + 'src/models/orchestrator/queues.models.ts', + 'src/models/common/request-spec.ts', + + // Simple utility files + 'src/utils/platform.ts', + 'src/core/config/config-utils.ts', + 'src/core/config/sdk-config.ts', + + // Infrastructure components (integration-heavy, better tested via e2e) + 'src/core/auth/**', + 'src/core/telemetry/**', ], }, },