Skip to content

Commit f3e59ba

Browse files
committed
Preserve custom_attributes and add unit tests
1 parent dad960c commit f3e59ba

2 files changed

Lines changed: 158 additions & 2 deletions

File tree

packages/clerk-js/src/core/resources/User.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ export class User extends BaseResource implements UserResource {
335335
await BaseResource._fetch<EnterpriseConnectionJSON>({
336336
path: `${this.path()}/enterprise_connections`,
337337
method: 'POST',
338-
body: deepCamelToSnake(params) as any,
338+
body: toMeEnterpriseConnectionBody(params) as any,
339339
})
340340
)?.response as unknown as EnterpriseConnectionJSON;
341341

@@ -350,7 +350,7 @@ export class User extends BaseResource implements UserResource {
350350
await BaseResource._fetch<EnterpriseConnectionJSON>({
351351
path: `${this.path()}/enterprise_connections/${enterpriseConnectionId}`,
352352
method: 'PATCH',
353-
body: deepCamelToSnake(params) as any,
353+
body: toMeEnterpriseConnectionBody(params) as any,
354354
})
355355
)?.response as unknown as EnterpriseConnectionJSON;
356356

@@ -548,3 +548,30 @@ export class User extends BaseResource implements UserResource {
548548
};
549549
}
550550
}
551+
552+
/**
553+
* Serializes `CreateMeEnterpriseConnectionParams` / `UpdateMeEnterpriseConnectionParams`
554+
* for the `/me/enterprise_connections` FAPI endpoints.
555+
*
556+
* Uses `deepCamelToSnake` but preserves `saml.attributeMapping` and `customAttributes` as-is. Their keys are
557+
* user-supplied data and must not be camel→snake transformed.
558+
*/
559+
function toMeEnterpriseConnectionBody(
560+
params: CreateMeEnterpriseConnectionParams | UpdateMeEnterpriseConnectionParams,
561+
): Record<string, unknown> {
562+
const originalAttributeMapping =
563+
params.saml && typeof params.saml === 'object' ? params.saml.attributeMapping : undefined;
564+
const originalCustomAttributes = 'customAttributes' in params ? params.customAttributes : undefined;
565+
566+
const body = deepCamelToSnake(params) as Record<string, any>;
567+
568+
if (originalAttributeMapping !== undefined && body.saml && typeof body.saml === 'object') {
569+
body.saml.attribute_mapping = originalAttributeMapping;
570+
}
571+
572+
if (originalCustomAttributes !== undefined) {
573+
body.custom_attributes = originalCustomAttributes;
574+
}
575+
576+
return body;
577+
}

packages/clerk-js/src/core/resources/__tests__/User.test.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,135 @@ describe('User', () => {
240240
});
241241
});
242242

243+
it('preserves `saml.attributeMapping` and `saml.customAttributes` keys when creating an enterprise connection', async () => {
244+
BaseResource._fetch = vi.fn().mockReturnValue(
245+
Promise.resolve({
246+
response: {
247+
id: 'ec_new',
248+
object: 'enterprise_connection' as const,
249+
name: 'New SSO',
250+
active: true,
251+
provider: 'saml_okta',
252+
logo_public_url: null,
253+
domains: [],
254+
organization_id: null,
255+
sync_user_attributes: true,
256+
disable_additional_identifications: false,
257+
allow_organization_account_linking: false,
258+
custom_attributes: [],
259+
oauth_config: null,
260+
saml_connection: null,
261+
created_at: 1,
262+
updated_at: 1,
263+
},
264+
}),
265+
);
266+
267+
const user = new User({
268+
email_addresses: [],
269+
phone_numbers: [],
270+
web3_wallets: [],
271+
external_accounts: [],
272+
} as unknown as UserJSON);
273+
274+
await user.createEnterpriseConnection({
275+
provider: 'saml_okta',
276+
name: 'New SSO',
277+
saml: {
278+
idpEntityId: 'https://idp.example.com',
279+
attributeMapping: {
280+
emailAddress: 'mail',
281+
firstName: 'givenName',
282+
'custom:role': 'role',
283+
},
284+
},
285+
});
286+
287+
// @ts-ignore
288+
expect(BaseResource._fetch).toHaveBeenCalledWith({
289+
method: 'POST',
290+
path: '/me/enterprise_connections',
291+
body: {
292+
provider: 'saml_okta',
293+
name: 'New SSO',
294+
saml: {
295+
idp_entity_id: 'https://idp.example.com',
296+
attribute_mapping: {
297+
emailAddress: 'mail',
298+
firstName: 'givenName',
299+
'custom:role': 'role',
300+
},
301+
},
302+
},
303+
});
304+
});
305+
306+
it('preserves `customAttributes` and `saml.attributeMapping` keys when updating an enterprise connection', async () => {
307+
// @ts-ignore
308+
BaseResource._fetch = vi.fn().mockReturnValue(
309+
Promise.resolve({
310+
response: {
311+
id: 'ec_123',
312+
object: 'enterprise_connection' as const,
313+
name: 'Updated',
314+
active: true,
315+
provider: 'saml_okta',
316+
logo_public_url: null,
317+
domains: [],
318+
organization_id: null,
319+
sync_user_attributes: true,
320+
disable_additional_identifications: false,
321+
allow_organization_account_linking: false,
322+
custom_attributes: [],
323+
oauth_config: null,
324+
saml_connection: null,
325+
created_at: 1,
326+
updated_at: 2,
327+
},
328+
}),
329+
);
330+
331+
const user = new User({
332+
email_addresses: [],
333+
phone_numbers: [],
334+
web3_wallets: [],
335+
external_accounts: [],
336+
} as unknown as UserJSON);
337+
338+
await user.updateEnterpriseConnection('ec_123', {
339+
customAttributes: {
340+
MyClaim: 'x',
341+
CustomValue: 'y',
342+
nestedCamelKey: { innerCamelKey: 'z' },
343+
},
344+
saml: {
345+
attributeMapping: {
346+
emailAddress: 'mail',
347+
firstName: 'givenName',
348+
},
349+
},
350+
});
351+
352+
// @ts-ignore
353+
expect(BaseResource._fetch).toHaveBeenCalledWith({
354+
method: 'PATCH',
355+
path: '/me/enterprise_connections/ec_123',
356+
body: {
357+
custom_attributes: {
358+
MyClaim: 'x',
359+
CustomValue: 'y',
360+
nestedCamelKey: { innerCamelKey: 'z' },
361+
},
362+
saml: {
363+
attribute_mapping: {
364+
emailAddress: 'mail',
365+
firstName: 'givenName',
366+
},
367+
},
368+
},
369+
});
370+
});
371+
243372
it('deletes an enterprise connection', async () => {
244373
const deletedJSON = {
245374
object: 'enterprise_connection',

0 commit comments

Comments
 (0)