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

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,163 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { CollectionViewAdapter } from '.';
3
+ import { ReadSelectedComparisonOperator, ReadSelectedOrderingDirection, ReadSelectedPropertyType } from '../../data';
4
+ import { dateInterval, searchTerm } from '../../utils';
5
+ describe('CollectionViewAdapter', () => {
6
+ let data;
7
+ let error;
8
+ let isLoading;
9
+ const callback = vi.fn((_isLoading, _data, _error) => {
10
+ isLoading = _isLoading;
11
+ data = _data;
12
+ error = _error;
13
+ });
14
+ const options = {
15
+ pagination: { useTotalItemCount: true, pageSize: 5, pageNumber: 1 },
16
+ ordering: { maxActiveOrderingColumns: 1, orderByPaths: [] },
17
+ search: { textSearchableProperties: ['name'], numericSearchableProperties: ['id'] },
18
+ };
19
+ let adapter;
20
+ const waitForData = async (timeout = 2000) => {
21
+ const start = Date.now();
22
+ while (isLoading !== false && Date.now() - start < timeout) {
23
+ await new Promise(res => setTimeout(res, 50));
24
+ }
25
+ };
26
+ const resetAdapterState = () => {
27
+ data = undefined;
28
+ error = undefined;
29
+ isLoading = undefined;
30
+ };
31
+ beforeEach(() => {
32
+ data = undefined;
33
+ error = undefined;
34
+ isLoading = undefined;
35
+ });
36
+ it('callback returns correct loading status and collection data', async () => {
37
+ options.pagination.useTotalItemCount = true;
38
+ adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
39
+ await waitForData();
40
+ expect(callback).toHaveBeenNthCalledWith(1, true, undefined, undefined);
41
+ expect(callback).toHaveBeenLastCalledWith(false, data, undefined);
42
+ expect(data).toBeDefined();
43
+ });
44
+ it('adapter options set correct ordering', async () => {
45
+ adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
46
+ await waitForData();
47
+ resetAdapterState();
48
+ adapter.setOrdering(['name'], ReadSelectedOrderingDirection.Descending);
49
+ await waitForData();
50
+ expect(adapter.getCurrentOrdering()).toEqual([
51
+ {
52
+ orderByPath: ['name'],
53
+ orderDirection: ReadSelectedOrderingDirection.Descending,
54
+ },
55
+ ]);
56
+ });
57
+ it('text search is applied to adapter', async () => {
58
+ const searchDefinition = searchTerm('Flu', ['name'], ['id']);
59
+ if (!searchDefinition)
60
+ throw new Error('searchDefinition is undefined');
61
+ adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
62
+ await waitForData();
63
+ resetAdapterState();
64
+ adapter.setSearchDefinition(searchDefinition);
65
+ await waitForData();
66
+ expect(data).toBeDefined();
67
+ expect(data.length).toBeGreaterThan(0);
68
+ for (const item of data) {
69
+ expect(item.name).toMatch(/flu/i);
70
+ }
71
+ });
72
+ it('numeric search is applied to adapter', async () => {
73
+ const searchDefinition = searchTerm('10000001', ['id'], ['id']);
74
+ if (!searchDefinition)
75
+ throw new Error('searchDefinition is undefined');
76
+ adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
77
+ await waitForData();
78
+ resetAdapterState();
79
+ adapter.setSearchDefinition(searchDefinition);
80
+ await waitForData();
81
+ expect(data).toBeDefined();
82
+ expect(data.length).toBe(1);
83
+ for (const item of data) {
84
+ expect(item.id).toBe(10000001);
85
+ }
86
+ });
87
+ it('applies date range filter from-to on the same date type property', async () => {
88
+ adapter = new CollectionViewAdapter('PetsSlim', callback, options);
89
+ const from = new Date('2017-01-01T00:00:00.000Z');
90
+ const to = new Date('2017-12-31T00:00:00.000Z');
91
+ const searchDefinition = dateInterval(from, to, ReadSelectedComparisonOperator.GreaterOrEqual, ReadSelectedComparisonOperator.LessOrEqual, ReadSelectedPropertyType.DateOnly, 'dateOfBirth');
92
+ if (!searchDefinition)
93
+ throw new Error('searchDefinition is undefined');
94
+ await waitForData();
95
+ resetAdapterState();
96
+ adapter.setSearchDefinition(searchDefinition);
97
+ await waitForData();
98
+ expect(data).toBeDefined();
99
+ });
100
+ it('applies pagination: page size and jump to page set skip/limit', async () => {
101
+ adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
102
+ await waitForData();
103
+ const firstPageData = data;
104
+ resetAdapterState();
105
+ adapter.jumpToPage(3);
106
+ await waitForData();
107
+ expect(data).toBeDefined();
108
+ expect(data.length).toBe(5);
109
+ // Check that data is different from first page
110
+ expect(data).not.toEqual(firstPageData);
111
+ });
112
+ it('calculates total when useTotalItemCount=false and last page is partial', async () => {
113
+ options.pagination.useTotalItemCount = false;
114
+ options.pagination.pageSize = 10;
115
+ adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
116
+ await waitForData();
117
+ const firstPageData = data;
118
+ resetAdapterState();
119
+ adapter.jumpToPage(2);
120
+ await waitForData();
121
+ expect(adapter.totalItemCount).toBe(firstPageData.length + data.length);
122
+ });
123
+ it('setOrdering correctly handles PropertyPathDto array comparison and limits columns', async () => {
124
+ options.ordering.maxActiveOrderingColumns = 2;
125
+ // Add first ordering
126
+ adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
127
+ adapter.setOrdering(['name'], ReadSelectedOrderingDirection.Descending);
128
+ await waitForData();
129
+ expect(adapter.getCurrentOrdering()).toEqual([
130
+ {
131
+ orderByPath: ['name'],
132
+ orderDirection: ReadSelectedOrderingDirection.Descending,
133
+ },
134
+ ]);
135
+ // Add second ordering
136
+ adapter.setOrdering(['id'], ReadSelectedOrderingDirection.Ascending);
137
+ await waitForData();
138
+ // Should have both orderings, with 'id' first (most recent)
139
+ expect(adapter.getCurrentOrdering()).toEqual([
140
+ {
141
+ orderByPath: ['id'],
142
+ orderDirection: ReadSelectedOrderingDirection.Ascending,
143
+ },
144
+ {
145
+ orderByPath: ['name'],
146
+ orderDirection: ReadSelectedOrderingDirection.Descending,
147
+ },
148
+ ]);
149
+ // // Add third ordering - should remove the oldest one (name)
150
+ adapter.setOrdering(['description'], ReadSelectedOrderingDirection.Descending);
151
+ await waitForData();
152
+ expect(adapter.getCurrentOrdering()).toEqual([
153
+ {
154
+ orderByPath: ['description'],
155
+ orderDirection: ReadSelectedOrderingDirection.Descending,
156
+ },
157
+ {
158
+ orderByPath: ['id'],
159
+ orderDirection: ReadSelectedOrderingDirection.Ascending,
160
+ },
161
+ ]);
162
+ });
163
+ });
@@ -20,16 +20,20 @@ export interface IGlobalSearchable<TKey> extends IIdentifiable<TKey> {
20
20
  export declare class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
21
21
  onChange: SingularEventTarget<string>;
22
22
  private _readClient;
23
- private _options;
23
+ options: SearchAdapterOptions<TDto>;
24
24
  private _isLoading;
25
25
  private _currentAbortController?;
26
26
  private _lastSearchedValue;
27
27
  private _typesToSearch;
28
28
  private _fetchResultsDataTimeout?;
29
29
  private _fetchResultsPromises;
30
+ private _totalCount;
30
31
  constructor(options: SearchAdapterOptions<TDto>);
31
32
  get searchTriggerMinLength(): number;
32
33
  get multiselect(): boolean;
34
+ get isLoading(): boolean;
35
+ get totalCount(): number;
36
+ get hasMoreRecords(): boolean;
33
37
  private _searchText;
34
38
  get searchText(): string;
35
39
  set searchText(searchText: string);
@@ -13,22 +13,32 @@ export class SearchAdapter {
13
13
  onChange = new SingularEventTarget();
14
14
  // inputs
15
15
  _readClient;
16
- _options;
16
+ options;
17
17
  _isLoading = false;
18
18
  _currentAbortController;
19
- _lastSearchedValue = '';
19
+ _lastSearchedValue = undefined;
20
20
  _typesToSearch = [];
21
21
  _fetchResultsDataTimeout;
22
22
  _fetchResultsPromises = [];
23
+ _totalCount = 0;
23
24
  constructor(options) {
24
- this._options = options;
25
+ this.options = options;
25
26
  this._readClient = new ApiReadControllerClient(options.controllerName);
26
27
  }
27
28
  get searchTriggerMinLength() {
28
- return this._options.searchTriggerMinLength;
29
+ return this.options.searchTriggerMinLength;
29
30
  }
30
31
  get multiselect() {
31
- return this._options.multiselect;
32
+ return this.options.multiselect;
33
+ }
34
+ get isLoading() {
35
+ return this._isLoading;
36
+ }
37
+ get totalCount() {
38
+ return this._totalCount;
39
+ }
40
+ get hasMoreRecords() {
41
+ return this._totalCount > this._searchResults.length;
32
42
  }
33
43
  _searchText = '';
34
44
  get searchText() {
@@ -60,7 +70,7 @@ export class SearchAdapter {
60
70
  this.selectedItems = [];
61
71
  }
62
72
  // selection mode
63
- if (this._options.multiselect) {
73
+ if (this.options.multiselect) {
64
74
  if (!this._selectedItems.find(s => s.id === item.id)) {
65
75
  this.selectedItems = [...this.selectedItems, item];
66
76
  }
@@ -86,18 +96,19 @@ export class SearchAdapter {
86
96
  this.onChange.dispatchEvent('onChange');
87
97
  }
88
98
  async _trySearch() {
89
- const options = this._options;
99
+ const options = this.options;
90
100
  this.searchResults = [];
91
101
  if (this._searchText.length < options.searchTriggerMinLength)
92
102
  return;
93
- if (this._searchText == this._lastSearchedValue)
103
+ if (this._searchText === this._lastSearchedValue)
94
104
  return;
95
105
  this._lastSearchedValue = this._searchText;
96
106
  this._typesToSearch =
97
- options.limitSearchToSelectedType && this._selectedItems.length ? [this._selectedItems[0].type] : this._options.typesToSearch;
107
+ options.limitSearchToSelectedType && this._selectedItems.length ? [this._selectedItems[0].type] : this.options.typesToSearch;
98
108
  this.searchResults = this._sortByName(await this._fetchResults(options.searchDebounceDelay));
99
109
  }
100
110
  async _fetchResults(debounceDelay) {
111
+ this._isLoading = true;
101
112
  return new Promise((resolve, reject) => {
102
113
  this._fetchResultsPromises.push({ resolve, reject });
103
114
  if (this._fetchResultsDataTimeout !== undefined) {
@@ -119,7 +130,6 @@ export class SearchAdapter {
119
130
  });
120
131
  }
121
132
  async _fetchResultsDebounced() {
122
- this._isLoading = true;
123
133
  let abortController;
124
134
  let caughtError;
125
135
  try {
@@ -139,6 +149,8 @@ export class SearchAdapter {
139
149
  if (!response.ok) {
140
150
  throw response.error;
141
151
  }
152
+ // Update total count from response metadata
153
+ this._totalCount = response.metadata?.totalCount ?? response.result.length;
142
154
  // Filter out selected items from new results
143
155
  const selectedIds = new Set((this.selectedItems || []).map(item => item.id));
144
156
  return response.result.filter(item => !selectedIds.has(item.id));
@@ -168,7 +180,7 @@ export class SearchAdapter {
168
180
  const definition = {
169
181
  paginationDefinition: {
170
182
  skip: 0,
171
- limit: this._options.resultsLimit,
183
+ limit: this.options.resultsLimit,
172
184
  },
173
185
  };
174
186
  // Apply ordering
@@ -206,8 +218,8 @@ export class SearchAdapter {
206
218
  },
207
219
  ];
208
220
  }
209
- if (this._options.additionalSearchDefinitions && this._options.additionalSearchDefinitions.length > 0) {
210
- definition.searchDefinition.searches = [...(definition.searchDefinition.searches ?? []), ...this._options.additionalSearchDefinitions];
221
+ if (this.options.additionalSearchDefinitions && this.options.additionalSearchDefinitions.length > 0) {
222
+ definition.searchDefinition.searches = [...(definition.searchDefinition.searches ?? []), ...this.options.additionalSearchDefinitions];
211
223
  }
212
224
  return definition;
213
225
  }
@@ -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';
@@ -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,34 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { ApiCrudControllerClient } from '.';
3
+ import { FetchHttpService } from '../HttpService';
4
+ describe('ApiCrudControllerClient', () => {
5
+ let client;
6
+ let fetchService;
7
+ const testId = 100;
8
+ beforeEach(() => {
9
+ fetchService = new FetchHttpService();
10
+ client = new ApiCrudControllerClient('/VehiclesSlim', fetchService);
11
+ });
12
+ it('should use injected HTTP service for create', async () => {
13
+ const newVehicle = { id: testId, brand: 'New Brand', model: 'New Model' };
14
+ const result = (await client.create(newVehicle));
15
+ expect(result.ok).toBe(true);
16
+ expect(result.result).toBeDefined();
17
+ expect(result.result.id).toBe(newVehicle.id);
18
+ expect(result.result.brand).toBe(newVehicle.brand);
19
+ expect(result.result.model).toBe(newVehicle.model);
20
+ });
21
+ it('should use injected HTTP service for update', async () => {
22
+ const updateVehicle = { id: testId, brand: 'Updated Brand', model: 'Updated Model' };
23
+ const result = (await client.update(testId, updateVehicle));
24
+ expect(result.ok).toBe(true);
25
+ expect(result.result).toBeDefined();
26
+ expect(result.result.id).toBe(updateVehicle.id);
27
+ expect(result.result.brand).toBe(updateVehicle.brand);
28
+ expect(result.result.model).toBe(updateVehicle.model);
29
+ });
30
+ it('should use injected HTTP service for delete', async () => {
31
+ const result = await client.delete(testId);
32
+ expect(result.ok).toBe(true);
33
+ });
34
+ });
@@ -0,0 +1,59 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { ApiReadControllerClient } from '.';
3
+ import { FetchHttpService } from '../HttpService';
4
+ import { ReadSelectedComparisonOperator, ReadSelectedLogicalOperator, ReadSelectedPropertyType } from '../../../data';
5
+ describe('ApiReadControllerClient', () => {
6
+ let client;
7
+ let fetchService;
8
+ beforeEach(() => {
9
+ fetchService = new FetchHttpService();
10
+ client = new ApiReadControllerClient('/VehiclesSlim', fetchService);
11
+ });
12
+ it('should use injected HTTP service for readAll', async () => {
13
+ const result = (await client.readAll());
14
+ expect(result.ok).toBe(true);
15
+ expect(result.result).toBeDefined();
16
+ });
17
+ it('should use injected HTTP service for readSingle', async () => {
18
+ const testId = 1;
19
+ const result = (await client.readSingle(testId));
20
+ expect(result.ok).toBe(true);
21
+ expect(result.result).toBeDefined();
22
+ expect(typeof result.result).toBe('object');
23
+ expect(result.result.id).toBe(testId);
24
+ });
25
+ it('should use injected HTTP service for readSelected', async () => {
26
+ const definition = {
27
+ searchDefinition: {
28
+ searches: [
29
+ {
30
+ propertyCriteria: [
31
+ {
32
+ propertyName: 'Brand',
33
+ comparisonOperator: ReadSelectedComparisonOperator.Equal,
34
+ valueType: ReadSelectedPropertyType.String,
35
+ value: 'Audi',
36
+ },
37
+ {
38
+ propertyName: 'Brand',
39
+ comparisonOperator: ReadSelectedComparisonOperator.Equal,
40
+ valueType: ReadSelectedPropertyType.String,
41
+ value: 'BMW',
42
+ },
43
+ ],
44
+ logicalOperator: ReadSelectedLogicalOperator.Or,
45
+ },
46
+ ],
47
+ logicalOperator: ReadSelectedLogicalOperator.And,
48
+ },
49
+ };
50
+ const result = (await client.readSelected(definition));
51
+ expect(result.ok).toBe(true);
52
+ expect(result.result).toBeDefined();
53
+ });
54
+ it('should handle HTTP errors properly', async () => {
55
+ const invalidClient = new ApiReadControllerClient('/InvalidPath', fetchService);
56
+ const result = await invalidClient.readAll();
57
+ expect(result.ok).toBe(false);
58
+ });
59
+ });
@@ -0,0 +1,52 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { FetchHttpService } from './FetchHttpService';
3
+ const baseUrl = 'http://localhost:5000/api';
4
+ const credentials = {
5
+ username: 'admin@test.com',
6
+ password: 'Password1234!',
7
+ };
8
+ describe('FetchHttpService', () => {
9
+ let fetchService;
10
+ beforeEach(() => {
11
+ fetchService = new FetchHttpService();
12
+ });
13
+ it('should make a GET request successfully', async () => {
14
+ const result = await fetchService.request(`${baseUrl}/AnythingManuallyMappedRead/ReadAll`);
15
+ expect(result.ok).toBe(true);
16
+ expect(result.status).toBe(200);
17
+ expect(result.statusText).toBe('OK');
18
+ const jsonData = await result.json();
19
+ expect(jsonData.result).toBeDefined();
20
+ expect(jsonData.error).toBeNull();
21
+ });
22
+ it('should make a POST request with body', async () => {
23
+ const result = await fetchService.request(`${baseUrl}/auth/signin`, {
24
+ method: 'POST',
25
+ headers: { 'Content-Type': 'application/json' },
26
+ body: JSON.stringify(credentials),
27
+ });
28
+ expect(result.ok).toBe(true);
29
+ expect(result.status).toBe(204);
30
+ });
31
+ it('should handle non-ok responses', async () => {
32
+ const result = await fetchService.request(`${baseUrl}/auth/signin`, {
33
+ method: 'POST',
34
+ headers: { 'Content-Type': 'application/json' },
35
+ body: JSON.stringify({ username: credentials.username, password: `${credentials.password}wrong` }),
36
+ });
37
+ expect(result.ok).toBe(false);
38
+ expect(result.status).toBe(400);
39
+ expect(result.statusText).toBe('Bad Request');
40
+ const jsonData = await result.json();
41
+ expect(jsonData.result).toBeNull();
42
+ expect(jsonData.error).toBeDefined();
43
+ expect(jsonData.error.code).toBe(400);
44
+ expect(jsonData.error.message).toBe('Invalid email or password');
45
+ });
46
+ it('should handle fetch errors from backend', async () => {
47
+ const result = await fetchService.request('http://localhost:5000/api/error');
48
+ expect(result.ok).toBe(false);
49
+ expect(result.status).toBe(404);
50
+ expect(result.statusText).toBe('Not Found');
51
+ });
52
+ });
@@ -30,12 +30,6 @@ export declare class UserManagementControllerClient<TKey, TResultDto extends Use
30
30
  * @returns List of claims assigned to the user
31
31
  */
32
32
  getUserClaims(id: TKey, signal?: AbortSignal): Promise<ApiResponseDto<ClaimDto[], ReadMetadataDto>>;
33
- /**
34
- * Gets all available roles in the system
35
- * @param {AbortSignal} [signal] - Optional cancellation signal
36
- * @returns List of all role names
37
- */
38
- getAllRoles(signal?: AbortSignal): Promise<ApiResponseDto<string[], ReadMetadataDto>>;
39
33
  /**
40
34
  * Changes the password for a user
41
35
  * @param id - The ID of the user
@@ -0,0 +1,60 @@
1
+ import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
+ import { UserManagementControllerClient } 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('UserManagementControllerClient', () => {
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 UserManagementControllerClient('/users', fetchService);
49
+ });
50
+ it('should use injected HTTP service for getUserRoles', async () => {
51
+ const result = (await client.getUserRoles(1));
52
+ expect(result.ok).toBe(true);
53
+ expect(result.result).toBeDefined();
54
+ });
55
+ it('should use injected HTTP service for getUserClaims', async () => {
56
+ const result = (await client.getUserClaims(1));
57
+ expect(result.ok).toBe(true);
58
+ expect(result.result).toBeDefined();
59
+ });
60
+ });
@@ -79,35 +79,6 @@ export class UserManagementControllerClient extends ApiCrudControllerClient {
79
79
  });
80
80
  }
81
81
  }
82
- /**
83
- * Gets all available roles in the system
84
- * @param {AbortSignal} [signal] - Optional cancellation signal
85
- * @returns List of all role names
86
- */
87
- async getAllRoles(signal) {
88
- try {
89
- const url = await apiInitializationService.getApiUrl(this.baseControllerPath, 'GetAllRoles');
90
- const res = await this.httpService.request(url, {
91
- method: 'GET',
92
- credentials: 'include',
93
- signal,
94
- });
95
- if (!res.ok) {
96
- return fail(await res.json());
97
- }
98
- return ok(await res.json());
99
- }
100
- catch (err) {
101
- console.error(err);
102
- return fail({
103
- error: {
104
- code: ErrorCode.UnknownError,
105
- message: 'Unknown error while fetching all roles',
106
- metadata: {},
107
- },
108
- });
109
- }
110
- }
111
82
  /**
112
83
  * Changes the password for a user
113
84
  * @param id - The ID of the user
@@ -1,5 +1,5 @@
1
1
  import { IHttpService } from '../../..';
2
- import { ApiResponseDto, EmptyMetadataDto, UserDto, UserInfoDto } from '../../../../data';
2
+ import { ApiResponseDto, EmptyMetadataDto, UserDto, UserInfoDto, RegisterRequestDto } from '../../../../data';
3
3
  export { UserDto, UserInfoDto };
4
4
  /**
5
5
  * AuthService class is responsible for managing user authentication operations.
@@ -8,7 +8,7 @@ export { UserDto, UserInfoDto };
8
8
  * @export
9
9
  * @class AuthService
10
10
  */
11
- export declare class AuthService<TKey, TUser extends UserDto<TKey> = UserDto<TKey>> {
11
+ export declare class AuthService<TKey, TRegisterUser extends RegisterRequestDto = RegisterRequestDto, TUser extends UserDto<TKey> = UserDto<TKey>> {
12
12
  protected readonly baseControllerPath: string;
13
13
  /**
14
14
  * Constructor
@@ -55,6 +55,14 @@ export declare class AuthService<TKey, TUser extends UserDto<TKey> = UserDto<TKe
55
55
  * @throws {Error} Error if fetch fails
56
56
  */
57
57
  signOut(): Promise<ApiResponseDto<undefined, EmptyMetadataDto>>;
58
+ /**
59
+ * Registers a new user by sending user details to the API.
60
+ *
61
+ * @param {TRegisterUser} user User object containing registration data (e.g. username, password, and optionally firstName, lastName, etc.)
62
+ * @return {Promise<ApiResponseDto<TUser, EmptyMetadataDto>>} API response with registered user details or error info
63
+ * @throws {Error} Error if the request fails
64
+ */
65
+ register(user: TRegisterUser): Promise<ApiResponseDto<TUser, EmptyMetadataDto>>;
58
66
  /**
59
67
  * If any API response returns an "Unauthenticated" response, call this method
60
68
  * to update local authentication state to unauthenticated
@@ -143,6 +143,46 @@ export class AuthService {
143
143
  });
144
144
  }
145
145
  }
146
+ /**
147
+ * Registers a new user by sending user details to the API.
148
+ *
149
+ * @param {TRegisterUser} user User object containing registration data (e.g. username, password, and optionally firstName, lastName, etc.)
150
+ * @return {Promise<ApiResponseDto<TUser, EmptyMetadataDto>>} API response with registered user details or error info
151
+ * @throws {Error} Error if the request fails
152
+ */
153
+ async register(user) {
154
+ try {
155
+ const url = await apiInitializationService.getApiUrl(this.baseControllerPath, '/register');
156
+ const res = await this.httpService.request(url, {
157
+ method: 'POST',
158
+ headers: { 'Content-Type': 'application/json' },
159
+ body: JSON.stringify(user),
160
+ credentials: 'include',
161
+ });
162
+ if (!res.ok) {
163
+ return fail(await res.json());
164
+ }
165
+ // TODO: Handle tokens if not using cookies
166
+ const whoAmIRes = await this.whoAmI();
167
+ if (!whoAmIRes.ok) {
168
+ return whoAmIRes;
169
+ }
170
+ return ok({
171
+ result: whoAmIRes.result.user,
172
+ metadata: whoAmIRes.metadata,
173
+ });
174
+ }
175
+ catch (err) {
176
+ console.error(err);
177
+ return fail({
178
+ error: {
179
+ code: ErrorCode.UnknownError,
180
+ message: 'Failed registering user',
181
+ metadata: {},
182
+ },
183
+ });
184
+ }
185
+ }
146
186
  /**
147
187
  * If any API response returns an "Unauthenticated" response, call this method
148
188
  * to update local authentication state to unauthenticated