Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 35 additions & 28 deletions packages/javascript/src/api/__tests__/getScim2Me.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,61 +21,68 @@ import ThunderIDAPIError from '../../errors/ThunderIDAPIError';
import getScim2Me from '../getScim2Me';

// Mock user data
const mockUser: Record<string, unknown> = {
email: 'test@example.com',
familyName: 'User',
givenName: 'Test',
const mockUserResponse: Record<string, unknown> = {
id: '123',
username: 'testuser',
attributes: {
email: 'test@example.com',
familyName: 'User',
givenName: 'Test',
username: 'testuser',
},
};

const mockUser: Record<string, unknown> = {
...mockUserResponse,
...mockUserResponse.attributes,
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

describe('getScim2Me', () => {
it('should fetch user profile successfully with default fetch', async () => {
// Mock fetch
const mockFetch: typeof fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve(mockUser),
json: () => Promise.resolve(mockUserResponse),
ok: true,
status: 200,
statusText: 'OK',
text: () => Promise.resolve(JSON.stringify(mockUser)),
text: () => Promise.resolve(JSON.stringify(mockUserResponse)),
});

// Replace global fetch
global.fetch = mockFetch;

const result: Record<string, unknown> = await getScim2Me({
url: 'https://localhost:8090/scim2/Me',
url: 'https://localhost:8090/users/me',
});

expect(result).toEqual(mockUser);
expect(mockFetch).toHaveBeenCalledWith('https://localhost:8090/scim2/Me', {
expect(mockFetch).toHaveBeenCalledWith('https://localhost:8090/users/me', {
headers: {
Accept: 'application/json',
'Content-Type': 'application/scim+json',
'Content-Type': 'application/json',
},
method: 'GET',
});
});

it('should use custom fetcher when provided', async () => {
const customFetcher: typeof fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve(mockUser),
json: () => Promise.resolve(mockUserResponse),
ok: true,
status: 200,
statusText: 'OK',
text: () => Promise.resolve(JSON.stringify(mockUser)),
text: () => Promise.resolve(JSON.stringify(mockUserResponse)),
});

const result: Record<string, unknown> = await getScim2Me({
fetcher: customFetcher,
url: 'https://localhost:8090/scim2/Me',
url: 'https://localhost:8090/users/me',
});

expect(result).toEqual(mockUser);
expect(customFetcher).toHaveBeenCalledWith('https://localhost:8090/scim2/Me', {
expect(customFetcher).toHaveBeenCalledWith('https://localhost:8090/users/me', {
headers: {
Accept: 'application/json',
'Content-Type': 'application/scim+json',
'Content-Type': 'application/json',
},
method: 'GET',
});
Expand All @@ -89,13 +96,13 @@ describe('getScim2Me', () => {
await expect(
getScim2Me({
fetcher: customFetcher,
url: 'https://localhost:8090/scim2/Me',
url: 'https://localhost:8090/users/me',
}),
).rejects.toThrow(ThunderIDAPIError);
await expect(
getScim2Me({
fetcher: customFetcher,
url: 'https://localhost:8090/scim2/Me',
url: 'https://localhost:8090/users/me',
}),
).rejects.toThrow('Network or parsing error: Custom fetcher failure');
});
Expand Down Expand Up @@ -153,7 +160,7 @@ describe('getScim2Me', () => {

await expect(
getScim2Me({
url: 'https://localhost:8090/scim2/Me',
url: 'https://localhost:8090/users/me',
}),
).rejects.toThrow(ThunderIDAPIError);
});
Expand All @@ -165,7 +172,7 @@ describe('getScim2Me', () => {

await expect(
getScim2Me({
url: 'https://localhost:8090/scim2/Me',
url: 'https://localhost:8090/users/me',
}),
).rejects.toThrow(ThunderIDAPIError);
});
Expand All @@ -181,11 +188,11 @@ describe('getScim2Me', () => {

it('should pass through custom headers', async () => {
const mockFetch: typeof fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve(mockUser),
json: () => Promise.resolve(mockUserResponse),
ok: true,
status: 200,
statusText: 'OK',
text: () => Promise.resolve(JSON.stringify(mockUser)),
text: () => Promise.resolve(JSON.stringify(mockUserResponse)),
});

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

await getScim2Me({
headers: customHeaders,
url: 'https://localhost:8090/scim2/Me',
url: 'https://localhost:8090/users/me',
});

expect(mockFetch).toHaveBeenCalledWith('https://localhost:8090/scim2/Me', {
expect(mockFetch).toHaveBeenCalledWith('https://localhost:8090/users/me', {
headers: {
Accept: 'application/json',
'Content-Type': 'application/scim+json',
'Content-Type': 'application/json',
...customHeaders,
},
method: 'GET',
Expand All @@ -211,22 +218,22 @@ describe('getScim2Me', () => {

it('should default to baseUrl if url is not provided', async () => {
const mockFetch: typeof fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve(mockUser),
json: () => Promise.resolve(mockUserResponse),
ok: true,
status: 200,
statusText: 'OK',
text: () => Promise.resolve(JSON.stringify(mockUser)),
text: () => Promise.resolve(JSON.stringify(mockUserResponse)),
});
global.fetch = mockFetch;

const baseUrl = 'https://localhost:8090';
await getScim2Me({
baseUrl,
});
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/scim2/Me`, {
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/users/me`, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/scim+json',
'Content-Type': 'application/json',
},
method: 'GET',
});
Expand Down
86 changes: 54 additions & 32 deletions packages/javascript/src/api/__tests__/updateMeProfile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,80 +27,104 @@ describe('updateMeProfile', (): void => {
});

it('should update profile successfully using default fetch', async (): Promise<void> => {
const mockUser: User = {
email: 'alice@example.com',
const mockUserResponse = {
id: 'u1',
name: 'Alice',
attributes: {
email: 'alice@example.com',
name: 'Alice',
},
};

const mockUser: User = {
...mockUserResponse,
...mockUserResponse.attributes,
};

global.fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve(mockUser),
json: () => Promise.resolve(mockUserResponse),
ok: true,
});

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

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

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

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

const parsed: Record<string, unknown> = JSON.parse(init.body as string);
expect(parsed.schemas).toEqual(['urn:ietf:params:scim:api:messages:2.0:PatchOp']);
expect(parsed.Operations).toEqual([{op: 'replace', value: payload}]);
expect(parsed.attributes).toEqual(payload);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

expect(result).toEqual(mockUser);
});

it('should fall back to baseUrl when url is not provided', async (): Promise<void> => {
const mockUser: User = {
email: 'bob@example.com',
const mockUserResponse = {
id: 'u2',
name: 'Bob',
attributes: {
email: 'bob@example.com',
name: 'Bob',
},
};

const mockUser: User = {
...mockUserResponse,
...mockUserResponse.attributes,
};

global.fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve(mockUser),
json: () => Promise.resolve(mockUserResponse),
ok: true,
});

const baseUrl = 'https://localhost:8090';
const payload: Record<string, unknown> = {profile: {givenName: 'Bob'}};
const payload: Record<string, unknown> = {givenName: 'Bob'};

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

expect(fetch).toHaveBeenCalledWith(`${baseUrl}/scim2/Me`, expect.any(Object));
expect(fetch).toHaveBeenCalledWith(`${baseUrl}/users/me`, expect.any(Object));
expect(result).toEqual(mockUser);
});

it('should use custom fetcher when provided', async (): Promise<void> => {
const mockUser: User = {email: 'carol@example.com', id: 'u3', name: 'Carol'};
const mockUserResponse = {
id: 'u3',
attributes: {
email: 'carol@example.com',
name: 'Carol',
},
};

const mockUser: User = {
...mockUserResponse,
...mockUserResponse.attributes,
};

const customFetcher: Mock = vi.fn().mockResolvedValue({
json: () => Promise.resolve(mockUser),
json: () => Promise.resolve(mockUserResponse),
ok: true,
});

const baseUrl = 'https://localhost:8090';
const payload: Record<string, unknown> = {profile: {familyName: 'Doe'}};
const payload: Record<string, unknown> = {familyName: 'Doe'};

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

expect(result).toEqual(mockUser);
expect(customFetcher).toHaveBeenCalledWith(
`${baseUrl}/scim2/Me`,
`${baseUrl}/users/me`,
expect.objectContaining({
headers: expect.objectContaining({
Accept: 'application/json',
'Content-Type': 'application/scim+json',
'Content-Type': 'application/json',
}),
method: 'PATCH',
method: 'PUT',
}),
);
});
Expand All @@ -112,7 +136,7 @@ describe('updateMeProfile', (): void => {
ok: true,
});

const url = 'https://localhost:8090/scim2/Me';
const url = 'https://localhost:8090/users/me';
const baseUrl = 'https://localhost:8090';
await updateMeProfile({baseUrl, payload: {x: 1}, url});

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

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

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

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

await updateMeProfile({baseUrl, payload});

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

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

it('should allow method override when provided in requestConfig', async (): Promise<void> => {
Expand Down
11 changes: 8 additions & 3 deletions packages/javascript/src/api/getScim2Me.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,13 @@ const getScim2Me = async ({url, baseUrl, fetcher, ...requestConfig}: GetScim2MeC
}

const fetchFn: typeof fetch = fetcher || fetch;
const resolvedUrl: string = url ?? `${baseUrl}/scim2/Me`;
const resolvedUrl: string = url ?? `${baseUrl}/users/me`;
Comment thread
janithjay marked this conversation as resolved.

const requestInit: RequestInit = {
...requestConfig,
headers: {
Accept: 'application/json',
'Content-Type': 'application/scim+json',
'Content-Type': 'application/json',
...requestConfig.headers,
},
method: 'GET',
Expand All @@ -134,8 +134,13 @@ const getScim2Me = async ({url, baseUrl, fetcher, ...requestConfig}: GetScim2MeC
}

const user: User = (await response.json()) as User;
const attributes: Record<string, unknown> = (user['attributes'] as Record<string, unknown>) ?? {};
const processedUser: User = {
...user,
...attributes,
};

return processUserUsername(user);
return processUserUsername(processedUser);
} catch (error) {
if (error instanceof ThunderIDAPIError) {
throw error;
Expand Down
Loading
Loading