@intellegens/cornerstone-client 0.0.9999-alpha-30 → 0.0.9999-alpha-31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/adapters/CollectionViewAdapter/index.integration.test.d.ts +1 -0
  2. package/dist/adapters/CollectionViewAdapter/index.integration.test.js +163 -0
  3. package/dist/adapters/SearchAdapter/index.d.ts +5 -1
  4. package/dist/adapters/SearchAdapter/index.js +25 -13
  5. package/dist/data/auth/dto/index.d.ts +1 -0
  6. package/dist/data/auth/dto/index.js +1 -0
  7. package/dist/services/api/ApiCrudControllerClient/index.integration.test.d.ts +1 -0
  8. package/dist/services/api/ApiCrudControllerClient/index.integration.test.js +34 -0
  9. package/dist/services/api/ApiReadControllerClient/index.integration.test.d.ts +1 -0
  10. package/dist/services/api/ApiReadControllerClient/index.integration.test.js +59 -0
  11. package/dist/services/api/HttpService/FetchHttpService.integration.test.d.ts +1 -0
  12. package/dist/services/api/HttpService/FetchHttpService.integration.test.js +52 -0
  13. package/dist/services/api/UserManagementControllerClient/index.d.ts +0 -6
  14. package/dist/services/api/UserManagementControllerClient/index.integration.test.d.ts +1 -0
  15. package/dist/services/api/UserManagementControllerClient/index.integration.test.js +60 -0
  16. package/dist/services/api/UserManagementControllerClient/index.js +0 -29
  17. package/dist/services/auth/client/AuthService/index.d.ts +10 -2
  18. package/dist/services/auth/client/AuthService/index.js +40 -0
  19. package/dist/services/auth/client/AuthorizationManagementControllerClient/index.integration.test.d.ts +1 -0
  20. package/dist/services/auth/client/AuthorizationManagementControllerClient/index.integration.test.js +89 -0
  21. package/package.json +6 -8
  22. package/src/adapters/CollectionViewAdapter/index.integration.test.ts +197 -0
  23. package/src/adapters/SearchAdapter/index.ts +30 -13
  24. package/src/data/api/dto/response/ApiSuccessResponseDto.ts +1 -1
  25. package/src/data/auth/dto/index.ts +1 -0
  26. package/src/services/api/ApiCrudControllerClient/index.integration.test.ts +46 -0
  27. package/src/services/api/ApiReadControllerClient/index.integration.test.ts +71 -0
  28. package/src/services/api/HttpService/FetchHttpService.integration.test.ts +65 -0
  29. package/src/services/api/UserManagementControllerClient/index.integration.test.ts +69 -0
  30. package/src/services/api/UserManagementControllerClient/index.ts +0 -32
  31. package/src/services/auth/client/AuthService/index.ts +48 -2
  32. package/src/services/auth/client/AuthorizationManagementControllerClient/index.integration.test.ts +110 -0
  33. package/vitest-setup.ts +43 -0
  34. package/vitest.config.ts +59 -0
  35. package/jest.config.js +0 -29
  36. package/tests/ApiClients.test.ts +0 -284
  37. package/tests/CollectionViewAdapter.test.ts +0 -392
  38. package/tests/HttpService.test.ts +0 -303
  39. package/tests/setup.ts +0 -76
@@ -0,0 +1,89 @@
1
+ import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
+ import { AuthorizationManagementControllerClient } from '.';
3
+ import fetchOrig from 'node-fetch';
4
+ import fetchCookie from 'fetch-cookie';
5
+ import { CookieJar } from 'tough-cookie';
6
+ // Create a fetch instance that handles auth cookies
7
+ const jar = new CookieJar();
8
+ const cookieFetch = fetchCookie(fetchOrig, jar);
9
+ // Custom IHttpService implementation for tests
10
+ class CookieFetchHttpService {
11
+ async request(url, config) {
12
+ const response = await cookieFetch(url, {
13
+ method: config?.method || 'GET',
14
+ headers: config?.headers,
15
+ body: config?.body,
16
+ signal: config?.signal,
17
+ });
18
+ const headers = {};
19
+ response.headers.forEach((value, key) => {
20
+ headers[key] = value;
21
+ });
22
+ return {
23
+ ok: response.ok,
24
+ status: response.status,
25
+ statusText: response.statusText,
26
+ headers,
27
+ json: () => response.json(),
28
+ text: () => response.text(),
29
+ };
30
+ }
31
+ }
32
+ const credentials = {
33
+ username: 'admin@test.com',
34
+ password: 'Password1234!',
35
+ };
36
+ const baseUrl = 'http://localhost:5000/api';
37
+ describe('AuthorizationManagementControllerClient', () => {
38
+ let client;
39
+ let fetchService = new CookieFetchHttpService();
40
+ beforeAll(async () => {
41
+ await fetchService.request(`${baseUrl}/auth/signin`, {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json' },
44
+ body: JSON.stringify(credentials),
45
+ });
46
+ });
47
+ beforeEach(() => {
48
+ client = new AuthorizationManagementControllerClient('/AuthorizationManagement', fetchService);
49
+ });
50
+ it('should fetch all roles', async () => {
51
+ const result = (await client.getAllRoles(false));
52
+ expect(result.ok).toBe(true);
53
+ expect(result.result).toBeDefined();
54
+ });
55
+ it('should fetch all roles with claims', async () => {
56
+ const result = (await client.getAllRoles(true));
57
+ expect(result.ok).toBe(true);
58
+ expect(result.result).toBeDefined();
59
+ expect(result.result[0].claims).toBeDefined();
60
+ });
61
+ it('should return role with claims by name', async () => {
62
+ const newRole = { name: 'New Role Test Claims', claims: [{ type: 'Test Claim Type', value: 'Test Claim Value' }] };
63
+ const create = (await client.upsertRole(newRole));
64
+ expect(create.ok).toBe(true);
65
+ const result = (await client.getRoleWithClaims(newRole.name));
66
+ expect(result.ok).toBe(true);
67
+ expect(result.result).toBeDefined();
68
+ expect(result.result.claims).toBeDefined();
69
+ expect(result.result.name).toBe(newRole.name);
70
+ expect(result.result.claims.length).toBe(newRole.claims.length);
71
+ expect(result.result.claims).toEqual(newRole.claims);
72
+ });
73
+ it('should create a new role with claims', async () => {
74
+ const newRole = { name: 'New Role', claims: [{ type: 'Test Claim Type', value: 'Test Claim Value' }] };
75
+ const result = (await client.upsertRole(newRole));
76
+ expect(result.ok).toBe(true);
77
+ expect(result.result).toBeDefined();
78
+ expect(result.result.name).toBe(newRole.name);
79
+ expect(result.result.claims.length).toBe(newRole.claims.length);
80
+ expect(result.result.claims).toEqual(newRole.claims);
81
+ });
82
+ it('should delete role', async () => {
83
+ const role = { name: 'Temp Role', claims: [] };
84
+ const create = (await client.upsertRole(role));
85
+ expect(create.ok).toBe(true);
86
+ const deleteResult = await client.deleteRole(role.name);
87
+ expect(deleteResult.ok).toBe(true);
88
+ });
89
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellegens/cornerstone-client",
3
- "version": "0.0.9999-alpha-30",
3
+ "version": "0.0.9999-alpha-31",
4
4
  "private": false,
5
5
  "publishable": true,
6
6
  "main": "./dist/index.js",
@@ -15,22 +15,18 @@
15
15
  ],
16
16
  "devDependencies": {
17
17
  "@eslint/js": "^9.21.0",
18
- "@jest/globals": "^30.2.0",
19
18
  "@types/express": "^5.0.0",
20
- "@types/jest": "^29.5.12",
21
19
  "@types/node": "^22.13.9",
22
20
  "eslint": "^9.21.0",
23
21
  "eslint-config-prettier": "^10.0.1",
24
22
  "express": "^4.21.2",
25
23
  "globals": "^16.0.0",
26
- "jest": "^29.7.0",
27
- "jest-environment-jsdom": "^29.7.0",
28
24
  "prettier": "^3.5.2",
29
25
  "rxjs": "~7.8.0",
30
- "ts-jest": "^29.1.2",
31
26
  "tsc-alias": "^1.8.11",
32
27
  "typescript-eslint": "^8.26.0",
33
28
  "vite": "6.3.4",
29
+ "vitest": "^3.2.4",
34
30
  "zod": "^3.24.2"
35
31
  },
36
32
  "dependencies": {
@@ -39,8 +35,10 @@
39
35
  },
40
36
  "scripts": {
41
37
  "clean": "pnpx rimraf '{dist}'",
42
- "test": "jest",
43
- "coverage": "jest --coverage",
38
+ "test": "vitest run --passWithNoTests",
39
+ "test:unit": "vitest run --project unit --passWithNoTests",
40
+ "test:integration": "vitest run --project integration --passWithNoTests",
41
+ "coverage": "vitest run --coverage",
44
42
  "build": "pnpm run clean && tsc && tsc-alias",
45
43
  "start": "pnpm run build && pnpx vite build && pnpx tsx ./demo"
46
44
  }
@@ -0,0 +1,197 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { CollectionViewAdapter, CollectionViewAdapterOptions } from '.';
3
+ import { ReadSelectedComparisonOperator, ReadSelectedOrderingDirection, ReadSelectedPropertyType } from '@/data';
4
+ import { dateInterval, searchTerm } from '@/utils';
5
+
6
+ type TestDto = { id: number; name: string; dateOfBirth: string; rowVersion: string };
7
+
8
+ describe('CollectionViewAdapter', () => {
9
+ let data: TestDto[] | undefined;
10
+ let error: string | undefined;
11
+ let isLoading: boolean | undefined;
12
+
13
+ const callback = vi.fn((_isLoading: boolean, _data: TestDto[] | undefined, _error: string | undefined) => {
14
+ isLoading = _isLoading;
15
+ data = _data;
16
+ error = _error;
17
+ });
18
+
19
+ const options: CollectionViewAdapterOptions<TestDto> = {
20
+ pagination: { useTotalItemCount: true, pageSize: 5, pageNumber: 1 },
21
+ ordering: { maxActiveOrderingColumns: 1, orderByPaths: [] },
22
+ search: { textSearchableProperties: ['name'], numericSearchableProperties: ['id'] },
23
+ };
24
+
25
+ let adapter: CollectionViewAdapter<number, TestDto, TestDto>;
26
+
27
+ const waitForData = async (timeout = 2000) => {
28
+ const start = Date.now();
29
+ while (isLoading !== false && Date.now() - start < timeout) {
30
+ await new Promise(res => setTimeout(res, 50));
31
+ }
32
+ };
33
+
34
+ const resetAdapterState = () => {
35
+ data = undefined;
36
+ error = undefined;
37
+ isLoading = undefined;
38
+ };
39
+
40
+ beforeEach(() => {
41
+ data = undefined;
42
+ error = undefined;
43
+ isLoading = undefined;
44
+ });
45
+
46
+ it('callback returns correct loading status and collection data', async () => {
47
+ options.pagination.useTotalItemCount = true;
48
+ adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
49
+ await waitForData();
50
+
51
+ expect(callback).toHaveBeenNthCalledWith(1, true, undefined, undefined);
52
+ expect(callback).toHaveBeenLastCalledWith(false, data, undefined);
53
+ expect(data).toBeDefined();
54
+ });
55
+
56
+ it('adapter options set correct ordering', async () => {
57
+ adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
58
+ await waitForData();
59
+ resetAdapterState();
60
+ adapter.setOrdering(['name'], ReadSelectedOrderingDirection.Descending);
61
+ await waitForData();
62
+
63
+ expect(adapter.getCurrentOrdering()).toEqual([
64
+ {
65
+ orderByPath: ['name'],
66
+ orderDirection: ReadSelectedOrderingDirection.Descending,
67
+ },
68
+ ]);
69
+ });
70
+
71
+ it('text search is applied to adapter', async () => {
72
+ const searchDefinition = searchTerm('Flu', ['name'], ['id']);
73
+ if (!searchDefinition) throw new Error('searchDefinition is undefined');
74
+ adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
75
+ await waitForData();
76
+ resetAdapterState();
77
+ adapter.setSearchDefinition(searchDefinition);
78
+ await waitForData();
79
+
80
+ expect(data).toBeDefined();
81
+ expect(data!.length).toBeGreaterThan(0);
82
+ for (const item of data!) {
83
+ expect(item.name).toMatch(/flu/i);
84
+ }
85
+ });
86
+
87
+ it('numeric search is applied to adapter', async () => {
88
+ const searchDefinition = searchTerm('10000001', ['id'], ['id']);
89
+ if (!searchDefinition) throw new Error('searchDefinition is undefined');
90
+ adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
91
+ await waitForData();
92
+ resetAdapterState();
93
+ adapter.setSearchDefinition(searchDefinition);
94
+ await waitForData();
95
+
96
+ expect(data).toBeDefined();
97
+ expect(data!.length).toBe(1);
98
+ for (const item of data!) {
99
+ expect(item.id).toBe(10000001);
100
+ }
101
+ });
102
+
103
+ it('applies date range filter from-to on the same date type property', async () => {
104
+ adapter = new CollectionViewAdapter('PetsSlim', callback, options);
105
+ const from = new Date('2017-01-01T00:00:00.000Z');
106
+ const to = new Date('2017-12-31T00:00:00.000Z');
107
+
108
+ const searchDefinition = dateInterval<TestDto>(
109
+ from,
110
+ to,
111
+ ReadSelectedComparisonOperator.GreaterOrEqual,
112
+ ReadSelectedComparisonOperator.LessOrEqual,
113
+ ReadSelectedPropertyType.DateOnly,
114
+ 'dateOfBirth',
115
+ );
116
+ if (!searchDefinition) throw new Error('searchDefinition is undefined');
117
+ await waitForData();
118
+ resetAdapterState();
119
+ adapter.setSearchDefinition(searchDefinition);
120
+ await waitForData();
121
+
122
+ expect(data).toBeDefined();
123
+ });
124
+
125
+ it('applies pagination: page size and jump to page set skip/limit', async () => {
126
+ adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
127
+ await waitForData();
128
+ const firstPageData = data;
129
+ resetAdapterState();
130
+ adapter.jumpToPage(3);
131
+ await waitForData();
132
+
133
+ expect(data).toBeDefined();
134
+ expect(data!.length).toBe(5);
135
+ // Check that data is different from first page
136
+ expect(data).not.toEqual(firstPageData);
137
+ });
138
+
139
+ it('calculates total when useTotalItemCount=false and last page is partial', async () => {
140
+ options.pagination.useTotalItemCount = false;
141
+ options.pagination.pageSize = 10;
142
+ adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
143
+ await waitForData();
144
+ const firstPageData = data;
145
+ resetAdapterState();
146
+ adapter.jumpToPage(2);
147
+ await waitForData();
148
+
149
+ expect(adapter.totalItemCount).toBe(firstPageData!.length + data!.length);
150
+ });
151
+
152
+ it('setOrdering correctly handles PropertyPathDto array comparison and limits columns', async () => {
153
+ options.ordering.maxActiveOrderingColumns = 2;
154
+ // Add first ordering
155
+ adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
156
+ adapter.setOrdering(['name'], ReadSelectedOrderingDirection.Descending);
157
+ await waitForData();
158
+
159
+ expect(adapter.getCurrentOrdering()).toEqual([
160
+ {
161
+ orderByPath: ['name'],
162
+ orderDirection: ReadSelectedOrderingDirection.Descending,
163
+ },
164
+ ]);
165
+
166
+ // Add second ordering
167
+ adapter.setOrdering(['id'], ReadSelectedOrderingDirection.Ascending);
168
+ await waitForData();
169
+
170
+ // Should have both orderings, with 'id' first (most recent)
171
+ expect(adapter.getCurrentOrdering()).toEqual([
172
+ {
173
+ orderByPath: ['id'],
174
+ orderDirection: ReadSelectedOrderingDirection.Ascending,
175
+ },
176
+ {
177
+ orderByPath: ['name'],
178
+ orderDirection: ReadSelectedOrderingDirection.Descending,
179
+ },
180
+ ]);
181
+
182
+ // // Add third ordering - should remove the oldest one (name)
183
+ adapter.setOrdering(['description'], ReadSelectedOrderingDirection.Descending);
184
+ await waitForData();
185
+
186
+ expect(adapter.getCurrentOrdering()).toEqual([
187
+ {
188
+ orderByPath: ['description'],
189
+ orderDirection: ReadSelectedOrderingDirection.Descending,
190
+ },
191
+ {
192
+ orderByPath: ['id'],
193
+ orderDirection: ReadSelectedOrderingDirection.Ascending,
194
+ },
195
+ ]);
196
+ });
197
+ });
@@ -41,28 +41,41 @@ export class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
41
41
  public onChange = new SingularEventTarget<string>();
42
42
  // inputs
43
43
  private _readClient!: ApiReadControllerClient<TKey, TDto, TDto>;
44
- private _options!: SearchAdapterOptions<TDto>;
44
+ public options!: SearchAdapterOptions<TDto>;
45
45
  private _isLoading = false;
46
46
  private _currentAbortController?: AbortController;
47
- private _lastSearchedValue = '';
47
+ private _lastSearchedValue: string | undefined = undefined;
48
48
  private _typesToSearch: string[] = [];
49
49
  private _fetchResultsDataTimeout?: any;
50
50
  private _fetchResultsPromises: {
51
51
  resolve: (value: TDto[] | PromiseLike<TDto[]>) => void;
52
52
  reject: (reason?: unknown) => void;
53
53
  }[] = [];
54
+ private _totalCount = 0;
54
55
 
55
56
  constructor(options: SearchAdapterOptions<TDto>) {
56
- this._options = options;
57
+ this.options = options;
57
58
  this._readClient = new ApiReadControllerClient<TKey, TDto, TDto>(options.controllerName);
58
59
  }
59
60
 
60
61
  public get searchTriggerMinLength() {
61
- return this._options.searchTriggerMinLength;
62
+ return this.options.searchTriggerMinLength;
62
63
  }
63
64
 
64
65
  public get multiselect() {
65
- return this._options.multiselect;
66
+ return this.options.multiselect;
67
+ }
68
+
69
+ public get isLoading() {
70
+ return this._isLoading;
71
+ }
72
+
73
+ public get totalCount() {
74
+ return this._totalCount;
75
+ }
76
+
77
+ public get hasMoreRecords() {
78
+ return this._totalCount > this._searchResults.length;
66
79
  }
67
80
 
68
81
  private _searchText: string = '';
@@ -105,7 +118,7 @@ export class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
105
118
  }
106
119
 
107
120
  // selection mode
108
- if (this._options.multiselect) {
121
+ if (this.options.multiselect) {
109
122
  if (!this._selectedItems.find(s => s.id === item.id)) {
110
123
  this.selectedItems = [...this.selectedItems, item];
111
124
  }
@@ -134,20 +147,22 @@ export class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
134
147
  }
135
148
 
136
149
  private async _trySearch() {
137
- const options = this._options;
150
+ const options = this.options;
138
151
 
139
152
  this.searchResults = [];
140
153
  if (this._searchText.length < options.searchTriggerMinLength) return;
141
- if (this._searchText == this._lastSearchedValue) return;
154
+ if (this._searchText === this._lastSearchedValue) return;
142
155
  this._lastSearchedValue = this._searchText;
143
156
 
144
157
  this._typesToSearch =
145
- options.limitSearchToSelectedType && this._selectedItems.length ? [(this._selectedItems[0] as TDto).type] : this._options.typesToSearch;
158
+ options.limitSearchToSelectedType && this._selectedItems.length ? [(this._selectedItems[0] as TDto).type] : this.options.typesToSearch;
146
159
 
147
160
  this.searchResults = this._sortByName(await this._fetchResults(options.searchDebounceDelay));
148
161
  }
149
162
 
150
163
  private async _fetchResults(debounceDelay: number): Promise<TDto[]> {
164
+ this._isLoading = true;
165
+
151
166
  return new Promise((resolve, reject) => {
152
167
  this._fetchResultsPromises.push({ resolve, reject });
153
168
  if (this._fetchResultsDataTimeout !== undefined) {
@@ -167,7 +182,6 @@ export class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
167
182
  }
168
183
 
169
184
  private async _fetchResultsDebounced(): Promise<TDto[]> {
170
- this._isLoading = true;
171
185
  let abortController: AbortController | undefined;
172
186
  let caughtError: unknown;
173
187
 
@@ -190,6 +204,9 @@ export class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
190
204
  throw response.error;
191
205
  }
192
206
 
207
+ // Update total count from response metadata
208
+ this._totalCount = response.metadata?.totalCount ?? response.result.length;
209
+
193
210
  // Filter out selected items from new results
194
211
  const selectedIds = new Set((this.selectedItems || []).map(item => item.id));
195
212
  return response.result.filter(item => !selectedIds.has(item.id));
@@ -222,7 +239,7 @@ export class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
222
239
  const definition: ReadSelectedDefinitionDto<TDto> = {
223
240
  paginationDefinition: {
224
241
  skip: 0,
225
- limit: this._options.resultsLimit,
242
+ limit: this.options.resultsLimit,
226
243
  },
227
244
  };
228
245
 
@@ -268,8 +285,8 @@ export class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
268
285
  ];
269
286
  }
270
287
 
271
- if (this._options.additionalSearchDefinitions && this._options.additionalSearchDefinitions.length > 0) {
272
- definition.searchDefinition.searches = [...(definition.searchDefinition.searches ?? []), ...this._options.additionalSearchDefinitions];
288
+ if (this.options.additionalSearchDefinitions && this.options.additionalSearchDefinitions.length > 0) {
289
+ definition.searchDefinition.searches = [...(definition.searchDefinition.searches ?? []), ...this.options.additionalSearchDefinitions];
273
290
  }
274
291
 
275
292
  return definition;
@@ -7,7 +7,7 @@
7
7
  * @property {TMetadata} metadata - Metadata about the result
8
8
  */
9
9
  export type ApiSuccessResponseDto<T, TMetadata> = {
10
- ok: true;
10
+ ok: true;
11
11
  result: T;
12
12
  metadata: TMetadata;
13
13
  };
@@ -2,3 +2,4 @@ export * from './ClaimDto';
2
2
  export * from './UserDto';
3
3
  export * from './UserInfoDto';
4
4
  export * from './RoleDto';
5
+ export * from './RegisterRequestDto';
@@ -0,0 +1,46 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { ApiCrudControllerClient } from '.';
3
+ import { FetchHttpService } from '../HttpService';
4
+
5
+ type VehicleSlimDto = { id: number; brand: string; model: string };
6
+
7
+ describe('ApiCrudControllerClient', () => {
8
+ let client: ApiCrudControllerClient<number, any, any, any, any>;
9
+ let fetchService: FetchHttpService;
10
+ const testId = 100;
11
+
12
+ beforeEach(() => {
13
+ fetchService = new FetchHttpService();
14
+ client = new ApiCrudControllerClient('/VehiclesSlim', fetchService);
15
+ });
16
+
17
+ it('should use injected HTTP service for create', async () => {
18
+ const newVehicle: VehicleSlimDto = { id: testId, brand: 'New Brand', model: 'New Model' };
19
+
20
+ const result = (await client.create(newVehicle)) as { ok: boolean; result: any };
21
+
22
+ expect(result.ok).toBe(true);
23
+ expect(result.result).toBeDefined();
24
+ expect(result.result.id).toBe(newVehicle.id);
25
+ expect(result.result.brand).toBe(newVehicle.brand);
26
+ expect(result.result.model).toBe(newVehicle.model);
27
+ });
28
+
29
+ it('should use injected HTTP service for update', async () => {
30
+ const updateVehicle: VehicleSlimDto = { id: testId, brand: 'Updated Brand', model: 'Updated Model' };
31
+
32
+ const result = (await client.update(testId, updateVehicle)) as { ok: boolean; result: any };
33
+
34
+ expect(result.ok).toBe(true);
35
+ expect(result.result).toBeDefined();
36
+ expect(result.result.id).toBe(updateVehicle.id);
37
+ expect(result.result.brand).toBe(updateVehicle.brand);
38
+ expect(result.result.model).toBe(updateVehicle.model);
39
+ });
40
+
41
+ it('should use injected HTTP service for delete', async () => {
42
+ const result = await client.delete(testId);
43
+
44
+ expect(result.ok).toBe(true);
45
+ });
46
+ });
@@ -0,0 +1,71 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { ApiReadControllerClient } from '.';
3
+ import { FetchHttpService } from '../HttpService';
4
+ import { ReadSelectedComparisonOperator, ReadSelectedDefinitionDto, ReadSelectedLogicalOperator, ReadSelectedPropertyType } from '@/data';
5
+
6
+ type VehicleSlimDto = { Id: number; Brand: string; Model: string; Year: number; RowVersion: string };
7
+
8
+ describe('ApiReadControllerClient', () => {
9
+ let client: ApiReadControllerClient<number, any, any>;
10
+ let fetchService: FetchHttpService;
11
+
12
+ beforeEach(() => {
13
+ fetchService = new FetchHttpService();
14
+ client = new ApiReadControllerClient('/VehiclesSlim', fetchService);
15
+ });
16
+
17
+ it('should use injected HTTP service for readAll', async () => {
18
+ const result = (await client.readAll()) as { ok: boolean; result: any };
19
+
20
+ expect(result.ok).toBe(true);
21
+ expect(result.result).toBeDefined();
22
+ });
23
+
24
+ it('should use injected HTTP service for readSingle', async () => {
25
+ const testId = 1;
26
+ const result = (await client.readSingle(testId)) as { ok: boolean; result: any };
27
+
28
+ expect(result.ok).toBe(true);
29
+ expect(result.result).toBeDefined();
30
+ expect(typeof result.result).toBe('object');
31
+ expect(result.result.id).toBe(testId);
32
+ });
33
+
34
+ it('should use injected HTTP service for readSelected', async () => {
35
+ const definition: ReadSelectedDefinitionDto<VehicleSlimDto> = {
36
+ searchDefinition: {
37
+ searches: [
38
+ {
39
+ propertyCriteria: [
40
+ {
41
+ propertyName: 'Brand',
42
+ comparisonOperator: ReadSelectedComparisonOperator.Equal,
43
+ valueType: ReadSelectedPropertyType.String,
44
+ value: 'Audi',
45
+ },
46
+ {
47
+ propertyName: 'Brand',
48
+ comparisonOperator: ReadSelectedComparisonOperator.Equal,
49
+ valueType: ReadSelectedPropertyType.String,
50
+ value: 'BMW',
51
+ },
52
+ ],
53
+ logicalOperator: ReadSelectedLogicalOperator.Or,
54
+ },
55
+ ],
56
+ logicalOperator: ReadSelectedLogicalOperator.And,
57
+ },
58
+ };
59
+
60
+ const result = (await client.readSelected(definition)) as { ok: boolean; result: any };
61
+
62
+ expect(result.ok).toBe(true);
63
+ expect(result.result).toBeDefined();
64
+ });
65
+
66
+ it('should handle HTTP errors properly', async () => {
67
+ const invalidClient = new ApiReadControllerClient('/InvalidPath', fetchService);
68
+ const result = await invalidClient.readAll();
69
+ expect(result.ok).toBe(false);
70
+ });
71
+ });
@@ -0,0 +1,65 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { FetchHttpService } from './FetchHttpService';
3
+
4
+ const baseUrl = 'http://localhost:5000/api';
5
+ const credentials = {
6
+ username: 'admin@test.com',
7
+ password: 'Password1234!',
8
+ };
9
+
10
+ describe('FetchHttpService', () => {
11
+ let fetchService: FetchHttpService;
12
+
13
+ beforeEach(() => {
14
+ fetchService = new FetchHttpService();
15
+ });
16
+
17
+ it('should make a GET request successfully', async () => {
18
+ const result = await fetchService.request(`${baseUrl}/AnythingManuallyMappedRead/ReadAll`);
19
+
20
+ expect(result.ok).toBe(true);
21
+ expect(result.status).toBe(200);
22
+ expect(result.statusText).toBe('OK');
23
+
24
+ const jsonData = await result.json();
25
+ expect(jsonData.result).toBeDefined();
26
+ expect(jsonData.error).toBeNull();
27
+ });
28
+
29
+ it('should make a POST request with body', async () => {
30
+ const result = await fetchService.request(`${baseUrl}/auth/signin`, {
31
+ method: 'POST',
32
+ headers: { 'Content-Type': 'application/json' },
33
+ body: JSON.stringify(credentials),
34
+ });
35
+
36
+ expect(result.ok).toBe(true);
37
+ expect(result.status).toBe(204);
38
+ });
39
+
40
+ it('should handle non-ok responses', async () => {
41
+ const result = await fetchService.request(`${baseUrl}/auth/signin`, {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json' },
44
+ body: JSON.stringify({ username: credentials.username, password: `${credentials.password}wrong` }),
45
+ });
46
+
47
+ expect(result.ok).toBe(false);
48
+ expect(result.status).toBe(400);
49
+ expect(result.statusText).toBe('Bad Request');
50
+
51
+ const jsonData = await result.json();
52
+ expect(jsonData.result).toBeNull();
53
+ expect(jsonData.error).toBeDefined();
54
+ expect(jsonData.error.code).toBe(400);
55
+ expect(jsonData.error.message).toBe('Invalid email or password');
56
+ });
57
+
58
+ it('should handle fetch errors from backend', async () => {
59
+ const result = await fetchService.request('http://localhost:5000/api/error');
60
+
61
+ expect(result.ok).toBe(false);
62
+ expect(result.status).toBe(404);
63
+ expect(result.statusText).toBe('Not Found');
64
+ });
65
+ });