Skip to content

Commit fbaff18

Browse files
committed
Update profile API endpoint to /users/me and switch to PUT/JSON format
1 parent a4f4283 commit fbaff18

14 files changed

Lines changed: 242 additions & 116 deletions

File tree

packages/javascript/src/api/__tests__/getScim2Me.test.ts

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,61 +21,68 @@ import ThunderIDAPIError from '../../errors/ThunderIDAPIError';
2121
import getScim2Me from '../getScim2Me';
2222

2323
// Mock user data
24-
const mockUser: Record<string, unknown> = {
25-
email: 'test@example.com',
26-
familyName: 'User',
27-
givenName: 'Test',
24+
const mockUserResponse: Record<string, unknown> = {
2825
id: '123',
29-
username: 'testuser',
26+
attributes: {
27+
email: 'test@example.com',
28+
familyName: 'User',
29+
givenName: 'Test',
30+
username: 'testuser',
31+
},
32+
};
33+
34+
const mockUser: Record<string, unknown> = {
35+
...mockUserResponse,
36+
...mockUserResponse.attributes,
3037
};
3138

3239
describe('getScim2Me', () => {
3340
it('should fetch user profile successfully with default fetch', async () => {
3441
// Mock fetch
3542
const mockFetch: typeof fetch = vi.fn().mockResolvedValue({
36-
json: () => Promise.resolve(mockUser),
43+
json: () => Promise.resolve(mockUserResponse),
3744
ok: true,
3845
status: 200,
3946
statusText: 'OK',
40-
text: () => Promise.resolve(JSON.stringify(mockUser)),
47+
text: () => Promise.resolve(JSON.stringify(mockUserResponse)),
4148
});
4249

4350
// Replace global fetch
4451
global.fetch = mockFetch;
4552

4653
const result: Record<string, unknown> = await getScim2Me({
47-
url: 'https://localhost:8090/scim2/Me',
54+
url: 'https://localhost:8090/users/me',
4855
});
4956

5057
expect(result).toEqual(mockUser);
51-
expect(mockFetch).toHaveBeenCalledWith('https://localhost:8090/scim2/Me', {
58+
expect(mockFetch).toHaveBeenCalledWith('https://localhost:8090/users/me', {
5259
headers: {
5360
Accept: 'application/json',
54-
'Content-Type': 'application/scim+json',
61+
'Content-Type': 'application/json',
5562
},
5663
method: 'GET',
5764
});
5865
});
5966

6067
it('should use custom fetcher when provided', async () => {
6168
const customFetcher: typeof fetch = vi.fn().mockResolvedValue({
62-
json: () => Promise.resolve(mockUser),
69+
json: () => Promise.resolve(mockUserResponse),
6370
ok: true,
6471
status: 200,
6572
statusText: 'OK',
66-
text: () => Promise.resolve(JSON.stringify(mockUser)),
73+
text: () => Promise.resolve(JSON.stringify(mockUserResponse)),
6774
});
6875

6976
const result: Record<string, unknown> = await getScim2Me({
7077
fetcher: customFetcher,
71-
url: 'https://localhost:8090/scim2/Me',
78+
url: 'https://localhost:8090/users/me',
7279
});
7380

7481
expect(result).toEqual(mockUser);
75-
expect(customFetcher).toHaveBeenCalledWith('https://localhost:8090/scim2/Me', {
82+
expect(customFetcher).toHaveBeenCalledWith('https://localhost:8090/users/me', {
7683
headers: {
7784
Accept: 'application/json',
78-
'Content-Type': 'application/scim+json',
85+
'Content-Type': 'application/json',
7986
},
8087
method: 'GET',
8188
});
@@ -89,13 +96,13 @@ describe('getScim2Me', () => {
8996
await expect(
9097
getScim2Me({
9198
fetcher: customFetcher,
92-
url: 'https://localhost:8090/scim2/Me',
99+
url: 'https://localhost:8090/users/me',
93100
}),
94101
).rejects.toThrow(ThunderIDAPIError);
95102
await expect(
96103
getScim2Me({
97104
fetcher: customFetcher,
98-
url: 'https://localhost:8090/scim2/Me',
105+
url: 'https://localhost:8090/users/me',
99106
}),
100107
).rejects.toThrow('Network or parsing error: Custom fetcher failure');
101108
});
@@ -153,7 +160,7 @@ describe('getScim2Me', () => {
153160

154161
await expect(
155162
getScim2Me({
156-
url: 'https://localhost:8090/scim2/Me',
163+
url: 'https://localhost:8090/users/me',
157164
}),
158165
).rejects.toThrow(ThunderIDAPIError);
159166
});
@@ -165,7 +172,7 @@ describe('getScim2Me', () => {
165172

166173
await expect(
167174
getScim2Me({
168-
url: 'https://localhost:8090/scim2/Me',
175+
url: 'https://localhost:8090/users/me',
169176
}),
170177
).rejects.toThrow(ThunderIDAPIError);
171178
});
@@ -181,11 +188,11 @@ describe('getScim2Me', () => {
181188

182189
it('should pass through custom headers', async () => {
183190
const mockFetch: typeof fetch = vi.fn().mockResolvedValue({
184-
json: () => Promise.resolve(mockUser),
191+
json: () => Promise.resolve(mockUserResponse),
185192
ok: true,
186193
status: 200,
187194
statusText: 'OK',
188-
text: () => Promise.resolve(JSON.stringify(mockUser)),
195+
text: () => Promise.resolve(JSON.stringify(mockUserResponse)),
189196
});
190197

191198
global.fetch = mockFetch;
@@ -196,13 +203,13 @@ describe('getScim2Me', () => {
196203

197204
await getScim2Me({
198205
headers: customHeaders,
199-
url: 'https://localhost:8090/scim2/Me',
206+
url: 'https://localhost:8090/users/me',
200207
});
201208

202-
expect(mockFetch).toHaveBeenCalledWith('https://localhost:8090/scim2/Me', {
209+
expect(mockFetch).toHaveBeenCalledWith('https://localhost:8090/users/me', {
203210
headers: {
204211
Accept: 'application/json',
205-
'Content-Type': 'application/scim+json',
212+
'Content-Type': 'application/json',
206213
...customHeaders,
207214
},
208215
method: 'GET',
@@ -211,22 +218,22 @@ describe('getScim2Me', () => {
211218

212219
it('should default to baseUrl if url is not provided', async () => {
213220
const mockFetch: typeof fetch = vi.fn().mockResolvedValue({
214-
json: () => Promise.resolve(mockUser),
221+
json: () => Promise.resolve(mockUserResponse),
215222
ok: true,
216223
status: 200,
217224
statusText: 'OK',
218-
text: () => Promise.resolve(JSON.stringify(mockUser)),
225+
text: () => Promise.resolve(JSON.stringify(mockUserResponse)),
219226
});
220227
global.fetch = mockFetch;
221228

222229
const baseUrl = 'https://localhost:8090';
223230
await getScim2Me({
224231
baseUrl,
225232
});
226-
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/scim2/Me`, {
233+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/users/me`, {
227234
headers: {
228235
Accept: 'application/json',
229-
'Content-Type': 'application/scim+json',
236+
'Content-Type': 'application/json',
230237
},
231238
method: 'GET',
232239
});

packages/javascript/src/api/__tests__/updateMeProfile.test.ts

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -27,80 +27,104 @@ describe('updateMeProfile', (): void => {
2727
});
2828

2929
it('should update profile successfully using default fetch', async (): Promise<void> => {
30-
const mockUser: User = {
31-
email: 'alice@example.com',
30+
const mockUserResponse = {
3231
id: 'u1',
33-
name: 'Alice',
32+
attributes: {
33+
email: 'alice@example.com',
34+
name: 'Alice',
35+
},
36+
};
37+
38+
const mockUser: User = {
39+
...mockUserResponse,
40+
...mockUserResponse.attributes,
3441
};
3542

3643
global.fetch = vi.fn().mockResolvedValue({
37-
json: () => Promise.resolve(mockUser),
44+
json: () => Promise.resolve(mockUserResponse),
3845
ok: true,
3946
});
4047

41-
const url = 'https://localhost:8090/scim2/Me';
42-
const payload: Record<string, unknown> = {'urn:scim:wso2:schema': {mobileNumbers: ['0777933830']}};
48+
const url = 'https://localhost:8090/users/me';
49+
const payload: Record<string, unknown> = {given_name: 'Alice'};
4350

4451
const result: User = await updateMeProfile({payload, url});
4552

4653
expect(fetch).toHaveBeenCalledTimes(1);
4754
const [calledUrl, init]: [string, RequestInit] = (fetch as unknown as Mock).mock.calls[0];
4855

4956
expect(calledUrl).toBe(url);
50-
expect(init.method).toBe('PATCH');
51-
expect((init.headers as Record<string, string>)['Content-Type']).toBe('application/scim+json');
57+
expect(init.method).toBe('PUT');
58+
expect((init.headers as Record<string, string>)['Content-Type']).toBe('application/json');
5259
expect((init.headers as Record<string, string>)['Accept']).toBe('application/json');
5360

5461
const parsed: Record<string, unknown> = JSON.parse(init.body as string);
55-
expect(parsed.schemas).toEqual(['urn:ietf:params:scim:api:messages:2.0:PatchOp']);
56-
expect(parsed.Operations).toEqual([{op: 'replace', value: payload}]);
62+
expect(parsed.attributes).toEqual(payload);
5763

5864
expect(result).toEqual(mockUser);
5965
});
6066

6167
it('should fall back to baseUrl when url is not provided', async (): Promise<void> => {
62-
const mockUser: User = {
63-
email: 'bob@example.com',
68+
const mockUserResponse = {
6469
id: 'u2',
65-
name: 'Bob',
70+
attributes: {
71+
email: 'bob@example.com',
72+
name: 'Bob',
73+
},
74+
};
75+
76+
const mockUser: User = {
77+
...mockUserResponse,
78+
...mockUserResponse.attributes,
6679
};
6780

6881
global.fetch = vi.fn().mockResolvedValue({
69-
json: () => Promise.resolve(mockUser),
82+
json: () => Promise.resolve(mockUserResponse),
7083
ok: true,
7184
});
7285

7386
const baseUrl = 'https://localhost:8090';
74-
const payload: Record<string, unknown> = {profile: {givenName: 'Bob'}};
87+
const payload: Record<string, unknown> = {givenName: 'Bob'};
7588

7689
const result: User = await updateMeProfile({baseUrl, payload});
7790

78-
expect(fetch).toHaveBeenCalledWith(`${baseUrl}/scim2/Me`, expect.any(Object));
91+
expect(fetch).toHaveBeenCalledWith(`${baseUrl}/users/me`, expect.any(Object));
7992
expect(result).toEqual(mockUser);
8093
});
8194

8295
it('should use custom fetcher when provided', async (): Promise<void> => {
83-
const mockUser: User = {email: 'carol@example.com', id: 'u3', name: 'Carol'};
96+
const mockUserResponse = {
97+
id: 'u3',
98+
attributes: {
99+
email: 'carol@example.com',
100+
name: 'Carol',
101+
},
102+
};
103+
104+
const mockUser: User = {
105+
...mockUserResponse,
106+
...mockUserResponse.attributes,
107+
};
84108

85109
const customFetcher: Mock = vi.fn().mockResolvedValue({
86-
json: () => Promise.resolve(mockUser),
110+
json: () => Promise.resolve(mockUserResponse),
87111
ok: true,
88112
});
89113

90114
const baseUrl = 'https://localhost:8090';
91-
const payload: Record<string, unknown> = {profile: {familyName: 'Doe'}};
115+
const payload: Record<string, unknown> = {familyName: 'Doe'};
92116

93117
const result: User = await updateMeProfile({baseUrl, fetcher: customFetcher, payload});
94118

95119
expect(result).toEqual(mockUser);
96120
expect(customFetcher).toHaveBeenCalledWith(
97-
`${baseUrl}/scim2/Me`,
121+
`${baseUrl}/users/me`,
98122
expect.objectContaining({
99123
headers: expect.objectContaining({
100124
Accept: 'application/json',
101-
'Content-Type': 'application/scim+json',
125+
'Content-Type': 'application/json',
102126
}),
103-
method: 'PATCH',
127+
method: 'PUT',
104128
}),
105129
);
106130
});
@@ -112,7 +136,7 @@ describe('updateMeProfile', (): void => {
112136
ok: true,
113137
});
114138

115-
const url = 'https://localhost:8090/scim2/Me';
139+
const url = 'https://localhost:8090/users/me';
116140
const baseUrl = 'https://localhost:8090';
117141
await updateMeProfile({baseUrl, payload: {x: 1}, url});
118142

@@ -156,16 +180,16 @@ describe('updateMeProfile', (): void => {
156180
it('should handle network or unknown errors with the generic message', async (): Promise<void> => {
157181
// Rejection with Error
158182
global.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
159-
await expect(updateMeProfile({payload: {a: 1}, url: 'https://localhost:8090/scim2/Me'})).rejects.toThrow(
183+
await expect(updateMeProfile({payload: {a: 1}, url: 'https://localhost:8090/users/me'})).rejects.toThrow(
160184
ThunderIDAPIError,
161185
);
162-
await expect(updateMeProfile({payload: {a: 1}, url: 'https://localhost:8090/scim2/Me'})).rejects.toThrow(
186+
await expect(updateMeProfile({payload: {a: 1}, url: 'https://localhost:8090/users/me'})).rejects.toThrow(
163187
'An error occurred while updating the user profile. Please try again.',
164188
);
165189

166190
// Rejection with non-Error
167191
global.fetch = vi.fn().mockRejectedValue('weird failure');
168-
await expect(updateMeProfile({payload: {a: 1}, url: 'https://localhost:8090/scim2/Me'})).rejects.toThrow(
192+
await expect(updateMeProfile({payload: {a: 1}, url: 'https://localhost:8090/users/me'})).rejects.toThrow(
169193
'An error occurred while updating the user profile. Please try again.',
170194
);
171195
});
@@ -192,28 +216,26 @@ describe('updateMeProfile', (): void => {
192216
expect((init as Record<string, unknown>).headers).toMatchObject({
193217
Accept: 'application/json',
194218
Authorization: 'Bearer token',
195-
'Content-Type': 'application/scim+json',
219+
'Content-Type': 'application/json',
196220
'X-Custom-Header': 'custom-value',
197221
});
198222
});
199223

200-
it('should build the SCIM PatchOp body correctly', async (): Promise<void> => {
224+
it('should build the PUT attributes body correctly', async (): Promise<void> => {
201225
global.fetch = vi.fn().mockResolvedValue({
202226
json: () => Promise.resolve({} as User),
203227
ok: true,
204228
});
205229

206230
const baseUrl = 'https://localhost:8090';
207-
const payload: Record<string, unknown> = {'urn:scim:wso2:schema': {mobileNumbers: ['123']}};
231+
const payload: Record<string, unknown> = {mobileNumbers: ['123']};
208232

209233
await updateMeProfile({baseUrl, payload});
210234

211235
const [, init]: [string, RequestInit] = (fetch as unknown as Mock).mock.calls[0];
212236
const body: Record<string, unknown> = JSON.parse((init as Record<string, unknown>).body as string);
213237

214-
expect(body.schemas).toEqual(['urn:ietf:params:scim:api:messages:2.0:PatchOp']);
215-
expect(body.Operations).toHaveLength(1);
216-
expect((body.Operations as Record<string, unknown>[])[0]).toEqual({op: 'replace', value: payload});
238+
expect(body.attributes).toEqual(payload);
217239
});
218240

219241
it('should allow method override when provided in requestConfig', async (): Promise<void> => {

packages/javascript/src/api/getScim2Me.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,13 @@ const getScim2Me = async ({url, baseUrl, fetcher, ...requestConfig}: GetScim2MeC
105105
}
106106

107107
const fetchFn: typeof fetch = fetcher || fetch;
108-
const resolvedUrl: string = url ?? `${baseUrl}/scim2/Me`;
108+
const resolvedUrl: string = url ?? `${baseUrl}/users/me`;
109109

110110
const requestInit: RequestInit = {
111111
...requestConfig,
112112
headers: {
113113
Accept: 'application/json',
114-
'Content-Type': 'application/scim+json',
114+
'Content-Type': 'application/json',
115115
...requestConfig.headers,
116116
},
117117
method: 'GET',
@@ -134,8 +134,12 @@ const getScim2Me = async ({url, baseUrl, fetcher, ...requestConfig}: GetScim2MeC
134134
}
135135

136136
const user: User = (await response.json()) as User;
137+
const processedUser = {
138+
...user,
139+
...user['attributes'],
140+
};
137141

138-
return processUserUsername(user);
142+
return processUserUsername(processedUser);
139143
} catch (error) {
140144
if (error instanceof ThunderIDAPIError) {
141145
throw error;

0 commit comments

Comments
 (0)