Skip to content

Commit 3fef2c5

Browse files
committed
Add unit tests for Asset Service
1 parent 06d7b69 commit 3fef2c5

File tree

6 files changed

+384
-0
lines changed

6 files changed

+384
-0
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
// ===== IMPORTS =====
2+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
3+
import { AssetService } from '../../../../src/services/orchestrator/assets';
4+
import { ApiClient } from '../../../../src/core/http/api-client';
5+
import { PaginationHelpers } from '../../../../src/utils/pagination/helpers';
6+
import {
7+
createMockRawAsset,
8+
createMockTransformedAssetCollection
9+
} from '../../../utils/mocks/assets';
10+
import { createServiceTestDependencies, createMockApiClient } from '../../../utils/setup';
11+
import { createMockError } from '../../../utils/mocks/core';
12+
import {
13+
AssetGetAllOptions,
14+
AssetGetByIdOptions,
15+
AssetValueType,
16+
AssetValueScope
17+
} from '../../../../src/models/orchestrator/assets.types';
18+
import { ASSET_TEST_CONSTANTS } from '../../../utils/constants/assets';
19+
import { TEST_CONSTANTS } from '../../../utils/constants/common';
20+
import { ASSET_ENDPOINTS } from '../../../../src/utils/constants/endpoints';
21+
import { FOLDER_ID } from '../../../../src/utils/constants/headers';
22+
23+
// ===== MOCKING =====
24+
// Mock the dependencies
25+
vi.mock('../../../../src/core/http/api-client');
26+
27+
// Import mock objects using vi.hoisted() - this ensures they're available before vi.mock() calls
28+
const mocks = vi.hoisted(() => {
29+
// Import/re-export the mock utilities from core
30+
return import('../../../utils/mocks/core');
31+
});
32+
33+
// Setup mocks at module level
34+
// NOTE: We do NOT mock transformData
35+
vi.mock('../../../../src/utils/pagination/helpers', async () => (await mocks).mockPaginationHelpers);
36+
37+
// ===== TEST SUITE =====
38+
describe('AssetService Unit Tests', () => {
39+
let assetService: AssetService;
40+
let mockApiClient: any;
41+
42+
beforeEach(() => {
43+
// Create mock instances using centralized setup
44+
const { config, executionContext, tokenManager } = createServiceTestDependencies();
45+
mockApiClient = createMockApiClient();
46+
47+
// Mock the ApiClient constructor
48+
vi.mocked(ApiClient).mockImplementation(() => mockApiClient);
49+
50+
// Reset pagination helpers mock before each test
51+
vi.mocked(PaginationHelpers.getAll).mockReset();
52+
53+
assetService = new AssetService(config, executionContext, tokenManager);
54+
});
55+
56+
afterEach(() => {
57+
vi.clearAllMocks();
58+
});
59+
60+
describe('getById', () => {
61+
it('should get asset by ID successfully with all fields mapped correctly', async () => {
62+
const mockAsset = createMockRawAsset();
63+
64+
mockApiClient.get.mockResolvedValue(mockAsset);
65+
66+
const result = await assetService.getById(
67+
ASSET_TEST_CONSTANTS.ASSET_ID,
68+
TEST_CONSTANTS.FOLDER_ID
69+
);
70+
71+
// Verify the result
72+
expect(result).toBeDefined();
73+
expect(result.id).toBe(ASSET_TEST_CONSTANTS.ASSET_ID);
74+
expect(result.name).toBe(ASSET_TEST_CONSTANTS.ASSET_NAME);
75+
expect(result.key).toBe(ASSET_TEST_CONSTANTS.ASSET_KEY);
76+
expect(result.valueType).toBe(AssetValueType.DBConnectionString,);
77+
expect(result.valueScope).toBe(AssetValueScope.Global,);
78+
79+
// Verify the API call has correct endpoint and headers
80+
expect(mockApiClient.get).toHaveBeenCalledWith(
81+
ASSET_ENDPOINTS.GET_BY_ID(ASSET_TEST_CONSTANTS.ASSET_ID),
82+
expect.objectContaining({
83+
headers: expect.objectContaining({
84+
[FOLDER_ID]: TEST_CONSTANTS.FOLDER_ID.toString()
85+
})
86+
})
87+
);
88+
89+
// Verify field transformations
90+
// CreationTime -> createdTime
91+
expect(result.createdTime).toBe(ASSET_TEST_CONSTANTS.CREATED_TIME);
92+
expect((result as any).CreationTime).toBeUndefined(); // Original field should be removed
93+
94+
// LastModificationTime -> lastModifiedTime
95+
expect(result.lastModifiedTime).toBe(ASSET_TEST_CONSTANTS.LAST_MODIFIED_TIME);
96+
expect((result as any).LastModificationTime).toBeUndefined(); // Original field should be removed
97+
});
98+
99+
it('should get asset with options successfully', async () => {
100+
const mockAsset = createMockRawAsset();
101+
mockApiClient.get.mockResolvedValue(mockAsset);
102+
103+
const options: AssetGetByIdOptions = {
104+
expand: ASSET_TEST_CONSTANTS.ODATA_EXPAND_KEY_VALUE_LIST,
105+
select: ASSET_TEST_CONSTANTS.ODATA_SELECT_FIELDS
106+
};
107+
108+
await assetService.getById(
109+
ASSET_TEST_CONSTANTS.ASSET_ID,
110+
TEST_CONSTANTS.FOLDER_ID,
111+
options
112+
);
113+
114+
// Verify API call has options with OData prefix
115+
expect(mockApiClient.get).toHaveBeenCalledWith(
116+
ASSET_ENDPOINTS.GET_BY_ID(ASSET_TEST_CONSTANTS.ASSET_ID),
117+
expect.objectContaining({
118+
params: expect.objectContaining({
119+
'$expand': ASSET_TEST_CONSTANTS.ODATA_EXPAND_KEY_VALUE_LIST,
120+
'$select': ASSET_TEST_CONSTANTS.ODATA_SELECT_FIELDS
121+
})
122+
})
123+
);
124+
});
125+
126+
it('should handle API errors', async () => {
127+
const error = createMockError(ASSET_TEST_CONSTANTS.ERROR_ASSET_NOT_FOUND);
128+
mockApiClient.get.mockRejectedValue(error);
129+
130+
await expect(assetService.getById(
131+
ASSET_TEST_CONSTANTS.ASSET_ID,
132+
TEST_CONSTANTS.FOLDER_ID
133+
)).rejects.toThrow(ASSET_TEST_CONSTANTS.ERROR_ASSET_NOT_FOUND);
134+
});
135+
});
136+
137+
describe('getAll', () => {
138+
it('should return all assets without pagination options', async () => {
139+
const mockResponse = createMockTransformedAssetCollection();
140+
141+
vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse);
142+
143+
const result = await assetService.getAll();
144+
145+
// Verify PaginationHelpers.getAll was called
146+
expect(PaginationHelpers.getAll).toHaveBeenCalledWith(
147+
expect.objectContaining({
148+
serviceAccess: expect.any(Object),
149+
getEndpoint: expect.toSatisfy((fn: Function) => fn() === ASSET_ENDPOINTS.GET_ALL),
150+
transformFn: expect.any(Function),
151+
pagination: expect.any(Object)
152+
}),
153+
undefined
154+
);
155+
156+
expect(result).toEqual(mockResponse);
157+
});
158+
159+
it('should return assets filtered by folder ID', async () => {
160+
const mockResponse = createMockTransformedAssetCollection();
161+
162+
vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse);
163+
164+
const options: AssetGetAllOptions = {
165+
folderId: TEST_CONSTANTS.FOLDER_ID
166+
};
167+
168+
const result = await assetService.getAll(options);
169+
170+
// Verify PaginationHelpers.getAll was called with folder options
171+
expect(PaginationHelpers.getAll).toHaveBeenCalledWith(
172+
expect.objectContaining({
173+
serviceAccess: expect.any(Object),
174+
getEndpoint: expect.toSatisfy((fn: Function) => fn(TEST_CONSTANTS.FOLDER_ID) === ASSET_ENDPOINTS.GET_BY_FOLDER),
175+
transformFn: expect.any(Function),
176+
pagination: expect.any(Object)
177+
}),
178+
expect.objectContaining({
179+
folderId: TEST_CONSTANTS.FOLDER_ID
180+
})
181+
);
182+
183+
expect(result).toEqual(mockResponse);
184+
});
185+
186+
it('should return paginated assets when pagination options provided', async () => {
187+
const mockResponse = createMockTransformedAssetCollection(100, {
188+
totalCount: 100,
189+
hasNextPage: true,
190+
nextCursor: TEST_CONSTANTS.NEXT_CURSOR,
191+
previousCursor: null,
192+
currentPage: 1,
193+
totalPages: 10
194+
});
195+
196+
vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse);
197+
198+
const options: AssetGetAllOptions = {
199+
pageSize: TEST_CONSTANTS.PAGE_SIZE
200+
};
201+
202+
const result = await assetService.getAll(options) as any;
203+
204+
// Verify PaginationHelpers.getAll was called with pagination options
205+
expect(PaginationHelpers.getAll).toHaveBeenCalledWith(
206+
expect.any(Object),
207+
expect.objectContaining({
208+
pageSize: TEST_CONSTANTS.PAGE_SIZE
209+
})
210+
);
211+
212+
expect(result).toEqual(mockResponse);
213+
expect(result.hasNextPage).toBe(true);
214+
expect(result.nextCursor).toBe(TEST_CONSTANTS.NEXT_CURSOR);
215+
});
216+
217+
it('should handle API errors', async () => {
218+
const error = createMockError(TEST_CONSTANTS.ERROR_MESSAGE);
219+
vi.mocked(PaginationHelpers.getAll).mockRejectedValue(error);
220+
221+
await expect(assetService.getAll()).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE);
222+
});
223+
});
224+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './assets.test';

tests/utils/constants/assets.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Asset service test constants
3+
* Asset-specific constants only
4+
*/
5+
6+
export const ASSET_TEST_CONSTANTS = {
7+
// Asset IDs
8+
ASSET_ID: 123,
9+
10+
// Asset Metadata
11+
ASSET_NAME: 'DatabaseConnection',
12+
ASSET_KEY: '12345678-1234-1234-1234-123456789abc',
13+
ASSET_DESCRIPTION: 'Database connection string for production',
14+
15+
// Asset Values
16+
ASSET_VALUE: 'Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;',
17+
18+
// Timestamps
19+
CREATED_TIME: '2023-10-15T10:00:00Z',
20+
LAST_MODIFIED_TIME: '2023-10-20T15:30:00Z',
21+
22+
// User IDs
23+
LAST_MODIFIER_USER_ID: 102,
24+
25+
// Key-Value List Items
26+
KEY_VALUE_ITEM_1_KEY: 'environment',
27+
KEY_VALUE_ITEM_1_VALUE: 'production',
28+
KEY_VALUE_ITEM_2_KEY: 'region',
29+
KEY_VALUE_ITEM_2_VALUE: 'us-east-1',
30+
31+
// Error Messages
32+
ERROR_ASSET_NOT_FOUND: 'Asset not found',
33+
34+
// OData Parameters
35+
ODATA_EXPAND_KEY_VALUE_LIST: 'keyValueList',
36+
ODATA_SELECT_FIELDS: 'id,name,value',
37+
} as const;

tests/utils/constants/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export * from './common';
66
export * from './maestro';
77
export * from './tasks';
88
export * from './entities';
9+
export * from './assets';
910

tests/utils/mocks/assets.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* Asset service mock utilities - Asset-specific mocks only
3+
* Uses generic utilities from core.ts for base functionality
4+
*/
5+
import { AssetValueScope, AssetValueType, AssetGetResponse } from '../../../src/models/orchestrator/assets.types';
6+
import { createMockBaseResponse, createMockCollection } from './core';
7+
import { ASSET_TEST_CONSTANTS } from '../constants/assets';
8+
import { TEST_CONSTANTS } from '../constants/common';
9+
10+
/**
11+
* Creates a mock asset with RAW API format (before transformation)
12+
* Uses PascalCase field names and raw API timestamp fields that need transformation
13+
*
14+
* @param overrides - Optional overrides for specific fields
15+
* @returns Raw asset data as it comes from the API (before transformation)
16+
*/
17+
export const createMockRawAsset = (overrides: Partial<any> = {}): any => {
18+
return createMockBaseResponse({
19+
Id: ASSET_TEST_CONSTANTS.ASSET_ID,
20+
Name: ASSET_TEST_CONSTANTS.ASSET_NAME,
21+
Key: ASSET_TEST_CONSTANTS.ASSET_KEY,
22+
Description: ASSET_TEST_CONSTANTS.ASSET_DESCRIPTION,
23+
ValueScope: AssetValueScope.Global,
24+
ValueType: AssetValueType.DBConnectionString,
25+
Value: ASSET_TEST_CONSTANTS.ASSET_VALUE,
26+
KeyValueList: [
27+
{
28+
Key: ASSET_TEST_CONSTANTS.KEY_VALUE_ITEM_1_KEY,
29+
Value: ASSET_TEST_CONSTANTS.KEY_VALUE_ITEM_1_VALUE
30+
},
31+
{
32+
Key: ASSET_TEST_CONSTANTS.KEY_VALUE_ITEM_2_KEY,
33+
Value: ASSET_TEST_CONSTANTS.KEY_VALUE_ITEM_2_VALUE
34+
}
35+
],
36+
CredentialStoreId: null,
37+
HasDefaultValue: true,
38+
CanBeDeleted: true,
39+
FoldersCount: 1,
40+
// Using raw API field names that should be transformed
41+
CreationTime: ASSET_TEST_CONSTANTS.CREATED_TIME,
42+
CreatorUserId: TEST_CONSTANTS.USER_ID,
43+
LastModificationTime: ASSET_TEST_CONSTANTS.LAST_MODIFIED_TIME,
44+
LastModifierUserId: ASSET_TEST_CONSTANTS.LAST_MODIFIER_USER_ID,
45+
}, overrides);
46+
};
47+
48+
/**
49+
* Creates a basic asset object with TRANSFORMED data (not raw API format)
50+
*
51+
* @param overrides - Optional overrides for specific fields
52+
* @returns Asset with transformed field names (camelCase)
53+
*/
54+
export const createBasicAsset = (overrides: Partial<AssetGetResponse> = {}): AssetGetResponse => {
55+
return createMockBaseResponse({
56+
id: ASSET_TEST_CONSTANTS.ASSET_ID,
57+
name: ASSET_TEST_CONSTANTS.ASSET_NAME,
58+
key: ASSET_TEST_CONSTANTS.ASSET_KEY,
59+
description: ASSET_TEST_CONSTANTS.ASSET_DESCRIPTION,
60+
valueScope: AssetValueScope.Global,
61+
valueType: AssetValueType.DBConnectionString,
62+
value: ASSET_TEST_CONSTANTS.ASSET_VALUE,
63+
keyValueList: [
64+
{
65+
key: ASSET_TEST_CONSTANTS.KEY_VALUE_ITEM_1_KEY,
66+
value: ASSET_TEST_CONSTANTS.KEY_VALUE_ITEM_1_VALUE
67+
},
68+
{
69+
key: ASSET_TEST_CONSTANTS.KEY_VALUE_ITEM_2_KEY,
70+
value: ASSET_TEST_CONSTANTS.KEY_VALUE_ITEM_2_VALUE
71+
}
72+
],
73+
credentialStoreId: null,
74+
hasDefaultValue: true,
75+
canBeDeleted: true,
76+
foldersCount: 1,
77+
// Using transformed field names (camelCase)
78+
createdTime: ASSET_TEST_CONSTANTS.CREATED_TIME,
79+
creatorUserId: TEST_CONSTANTS.USER_ID,
80+
lastModifiedTime: ASSET_TEST_CONSTANTS.LAST_MODIFIED_TIME,
81+
lastModifierUserId: ASSET_TEST_CONSTANTS.LAST_MODIFIER_USER_ID,
82+
}, overrides);
83+
};
84+
85+
86+
/**
87+
* Creates a mock transformed asset collection response as returned by PaginationHelpers.getAll
88+
*
89+
* @param count - Number of assets to include (defaults to 1)
90+
* @param options - Additional options like totalCount, pagination details
91+
* @returns Mock transformed asset collection with items array
92+
*/
93+
export const createMockTransformedAssetCollection = (
94+
count: number = 1,
95+
options?: {
96+
totalCount?: number;
97+
hasNextPage?: boolean;
98+
nextCursor?: string;
99+
previousCursor?: string | null;
100+
currentPage?: number;
101+
totalPages?: number;
102+
}
103+
): any => {
104+
const items = createMockCollection(count, (index) => createBasicAsset({
105+
id: ASSET_TEST_CONSTANTS.ASSET_ID + index,
106+
name: `${ASSET_TEST_CONSTANTS.ASSET_NAME}${index + 1}`,
107+
// Generate unique GUIDs for each asset
108+
key: `${index}-${ASSET_TEST_CONSTANTS.ASSET_KEY}`
109+
}));
110+
111+
return createMockBaseResponse({
112+
items,
113+
totalCount: options?.totalCount || count,
114+
...(options?.hasNextPage !== undefined && { hasNextPage: options.hasNextPage }),
115+
...(options?.nextCursor && { nextCursor: options.nextCursor }),
116+
...(options?.previousCursor !== undefined && { previousCursor: options.previousCursor }),
117+
...(options?.currentPage !== undefined && { currentPage: options.currentPage }),
118+
...(options?.totalPages !== undefined && { totalPages: options.totalPages })
119+
});
120+
};

0 commit comments

Comments
 (0)