Skip to content

Commit 1f43bf7

Browse files
authored
fix(backend): Fix casing of enterprise connection API params (#8022)
1 parent 02ff4f2 commit 1f43bf7

3 files changed

Lines changed: 210 additions & 0 deletions

File tree

.changeset/better-tires-repeat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/backend': patch
3+
---
4+
5+
Fix casing of enterprise connection API params when sending `saml` or `oidc` configuration
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { http, HttpResponse } from 'msw';
2+
import { describe, expect, it } from 'vitest';
3+
4+
import { server, validateHeaders } from '../../mock-server';
5+
import { createBackendApiClient } from '../factory';
6+
7+
describe('EnterpriseConnectionAPI', () => {
8+
const apiClient = createBackendApiClient({
9+
apiUrl: 'https://api.clerk.test',
10+
secretKey: 'deadbeef',
11+
});
12+
13+
const mockEnterpriseConnectionResponse = {
14+
object: 'enterprise_connection',
15+
id: 'entconn_123',
16+
name: 'Clerk',
17+
domains: ['clerk.dev'],
18+
organization_id: null,
19+
created_at: 1672531200000,
20+
updated_at: 1672531200000,
21+
active: true,
22+
sync_user_attributes: false,
23+
allow_subdomains: false,
24+
disable_additional_identifications: false,
25+
};
26+
27+
describe('createEnterpriseConnection', () => {
28+
it('sends nested saml params in snake_case', async () => {
29+
server.use(
30+
http.post(
31+
'https://api.clerk.test/v1/enterprise_connections',
32+
validateHeaders(async ({ request }) => {
33+
const body = (await request.json()) as Record<string, unknown>;
34+
35+
expect(body.name).toBe('Clerk');
36+
expect(body.domains).toEqual(['clerk.dev']);
37+
expect(body.saml).toEqual({
38+
idp_entity_id: 'xxx',
39+
idp_metadata_url: 'https://oauth.devsuccess.app/metadata',
40+
idp_sso_url: 'https://oauth.devsuccess.app/sso',
41+
});
42+
43+
return HttpResponse.json(mockEnterpriseConnectionResponse);
44+
}),
45+
),
46+
);
47+
48+
await apiClient.enterpriseConnections.createEnterpriseConnection({
49+
name: 'Clerk',
50+
domains: ['clerk.dev'],
51+
saml: {
52+
idpEntityId: 'xxx',
53+
idpMetadataUrl: 'https://oauth.devsuccess.app/metadata',
54+
idpSsoUrl: 'https://oauth.devsuccess.app/sso',
55+
},
56+
});
57+
});
58+
59+
it('sends nested oidc params', async () => {
60+
server.use(
61+
http.post(
62+
'https://api.clerk.test/v1/enterprise_connections',
63+
validateHeaders(async ({ request }) => {
64+
const body = (await request.json()) as Record<string, unknown>;
65+
66+
expect(body.oidc).toEqual({
67+
discovery_url: 'https://oidc.example.com/.well-known/openid-configuration',
68+
client_id: 'client_123',
69+
client_secret: 'secret_456',
70+
auth_url: 'https://oidc.example.com/authorize',
71+
token_url: 'https://oidc.example.com/token',
72+
user_info_url: 'https://oidc.example.com/userinfo',
73+
requires_pkce: true,
74+
});
75+
76+
return HttpResponse.json(mockEnterpriseConnectionResponse);
77+
}),
78+
),
79+
);
80+
81+
await apiClient.enterpriseConnections.createEnterpriseConnection({
82+
name: 'OIDC Connection',
83+
domains: ['example.com'],
84+
oidc: {
85+
discoveryUrl: 'https://oidc.example.com/.well-known/openid-configuration',
86+
clientId: 'client_123',
87+
clientSecret: 'secret_456',
88+
authUrl: 'https://oidc.example.com/authorize',
89+
tokenUrl: 'https://oidc.example.com/token',
90+
userInfoUrl: 'https://oidc.example.com/userinfo',
91+
requiresPkce: true,
92+
},
93+
});
94+
});
95+
});
96+
97+
describe('updateEnterpriseConnection', () => {
98+
it('sends nested saml params', async () => {
99+
server.use(
100+
http.patch(
101+
'https://api.clerk.test/v1/enterprise_connections/entconn_123',
102+
validateHeaders(async ({ request }) => {
103+
const body = (await request.json()) as Record<string, unknown>;
104+
105+
expect(body).toHaveProperty('saml');
106+
expect((body.saml as Record<string, unknown>).idp_entity_id).toBe('updated_entity');
107+
expect((body.saml as Record<string, unknown>).idp_metadata_url).toBe(
108+
'https://updated.example.com/metadata',
109+
);
110+
111+
return HttpResponse.json(mockEnterpriseConnectionResponse);
112+
}),
113+
),
114+
);
115+
116+
await apiClient.enterpriseConnections.updateEnterpriseConnection('entconn_123', {
117+
saml: {
118+
idpEntityId: 'updated_entity',
119+
idpMetadataUrl: 'https://updated.example.com/metadata',
120+
},
121+
});
122+
});
123+
});
124+
125+
describe('getEnterpriseConnectionList', () => {
126+
it('successfully fetches enterprise connections with query params in snake_case', async () => {
127+
const mockListResponse = {
128+
data: [mockEnterpriseConnectionResponse],
129+
total_count: 1,
130+
};
131+
132+
let capturedRequestUrl: string | null = null;
133+
server.use(
134+
http.get(
135+
'https://api.clerk.test/v1/enterprise_connections',
136+
validateHeaders(({ request }) => {
137+
capturedRequestUrl = request.url;
138+
return HttpResponse.json(mockListResponse);
139+
}),
140+
),
141+
);
142+
143+
const response = await apiClient.enterpriseConnections.getEnterpriseConnectionList({
144+
organizationId: 'org_123',
145+
active: true,
146+
limit: 10,
147+
offset: 5,
148+
});
149+
150+
expect(capturedRequestUrl).toBeTruthy();
151+
const url = new URL(capturedRequestUrl!);
152+
expect(url.searchParams.get('organization_id')).toBe('org_123');
153+
expect(url.searchParams.get('active')).toBe('true');
154+
expect(url.searchParams.get('limit')).toBe('10');
155+
expect(url.searchParams.get('offset')).toBe('5');
156+
157+
expect(response.data).toHaveLength(1);
158+
expect(response.data[0].id).toBe('entconn_123');
159+
expect(response.data[0].name).toBe('Clerk');
160+
expect(response.data[0].domains).toEqual(['clerk.dev']);
161+
expect(response.totalCount).toBe(1);
162+
});
163+
});
164+
165+
describe('getEnterpriseConnection', () => {
166+
it('successfully fetches a single enterprise connection', async () => {
167+
server.use(
168+
http.get(
169+
'https://api.clerk.test/v1/enterprise_connections/entconn_123',
170+
validateHeaders(() => HttpResponse.json(mockEnterpriseConnectionResponse)),
171+
),
172+
);
173+
174+
const response = await apiClient.enterpriseConnections.getEnterpriseConnection('entconn_123');
175+
176+
expect(response.id).toBe('entconn_123');
177+
expect(response.name).toBe('Clerk');
178+
expect(response.domains).toEqual(['clerk.dev']);
179+
expect(response.active).toBe(true);
180+
expect(response.organizationId).toBeNull();
181+
});
182+
});
183+
184+
describe('deleteEnterpriseConnection', () => {
185+
it('successfully deletes an enterprise connection', async () => {
186+
server.use(
187+
http.delete(
188+
'https://api.clerk.test/v1/enterprise_connections/entconn_123',
189+
validateHeaders(() => HttpResponse.json(mockEnterpriseConnectionResponse)),
190+
),
191+
);
192+
193+
const response = await apiClient.enterpriseConnections.deleteEnterpriseConnection('entconn_123');
194+
195+
expect(response.id).toBe('entconn_123');
196+
expect(response.name).toBe('Clerk');
197+
});
198+
});
199+
});

packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ export class EnterpriseConnectionAPI extends AbstractAPI {
6868
method: 'POST',
6969
path: basePath,
7070
bodyParams: params,
71+
options: {
72+
deepSnakecaseBodyParamKeys: true,
73+
},
7174
});
7275
}
7376

@@ -77,6 +80,9 @@ export class EnterpriseConnectionAPI extends AbstractAPI {
7780
method: 'PATCH',
7881
path: joinPaths(basePath, enterpriseConnectionId),
7982
bodyParams: params,
83+
options: {
84+
deepSnakecaseBodyParamKeys: true,
85+
},
8086
});
8187
}
8288

0 commit comments

Comments
 (0)