Skip to content

Commit c311e65

Browse files
committed
feat(backend): add enterprise connection test run methods
1 parent 6969cde commit c311e65

8 files changed

Lines changed: 381 additions & 1 deletion

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@clerk/backend': minor
3+
---
4+
5+
Add `createEnterpriseConnectionTestRun()` and `getEnterpriseConnectionTestRunList()` to `clerkClient.enterpriseConnections` for testing a SAML or OIDC enterprise connection against its Identity Provider (IdP). `createEnterpriseConnectionTestRun()` returns a short-lived `url` that starts a test sign-in. `getEnterpriseConnectionTestRunList()` returns the connection's test runs as `EnterpriseConnectionTestRun` objects, with each run's status, the user information parsed from the IdP and its log entries, and accepts a `status` filter plus `limit` and `offset`. `EnterpriseConnectionTestRun` and its related types are exported from `@clerk/backend`.
6+
7+
```ts
8+
const { url } = await clerkClient.enterpriseConnections.createEnterpriseConnectionTestRun('entconn_123');
9+
10+
const { data } = await clerkClient.enterpriseConnections.getEnterpriseConnectionTestRunList('entconn_123', {
11+
status: ['failed'],
12+
});
13+
```

‎.typedoc/custom-plugin.mjs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,20 @@ const LINK_REPLACEMENTS = [
110110
'enterprise-connection-saml-connection-login-hint',
111111
'/docs/reference/backend/types/backend-enterprise-connection-saml-connection-login-hint',
112112
],
113+
['enterprise-connection-test-run', '/docs/reference/backend/types/backend-enterprise-connection-test-run'],
114+
['enterprise-connection-test-run-log', '/docs/reference/backend/types/backend-enterprise-connection-test-run-log'],
115+
[
116+
'enterprise-connection-test-run-oauth-payload',
117+
'/docs/reference/backend/types/backend-enterprise-connection-test-run-oauth-payload',
118+
],
119+
[
120+
'enterprise-connection-test-run-parsed-user-info',
121+
'/docs/reference/backend/types/backend-enterprise-connection-test-run-parsed-user-info',
122+
],
123+
[
124+
'enterprise-connection-test-run-saml-payload',
125+
'/docs/reference/backend/types/backend-enterprise-connection-test-run-saml-payload',
126+
],
113127
['saml-connection-idp-certificate', '/docs/reference/backend/types/backend-saml-connection-idp-certificate'],
114128
['external-account', '/docs/reference/backend/types/backend-external-account'],
115129
['phone-number', '/docs/reference/backend/types/backend-phone-number'],

‎packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { http, HttpResponse } from 'msw';
22
import { describe, expect, expectTypeOf, it } from 'vitest';
33

4+
import type { EnterpriseConnectionTestRun as PublicEnterpriseConnectionTestRun } from '../../index';
45
import { server, validateHeaders } from '../../mock-server';
56
import type {
67
CreateEnterpriseConnectionParams,
78
EnterpriseConnectionSamlLoginHintParams,
89
} from '../endpoints/EnterpriseConnectionApi';
910
import { createBackendApiClient } from '../factory';
11+
import { EnterpriseConnectionTestRun } from '../resources';
1012

1113
describe('EnterpriseConnectionAPI', () => {
1214
const apiClient = createBackendApiClient({
@@ -410,4 +412,135 @@ describe('EnterpriseConnectionAPI', () => {
410412
expect(response.name).toBe('Clerk');
411413
});
412414
});
415+
416+
describe('createEnterpriseConnectionTestRun', () => {
417+
it('starts a test run and returns its URL', async () => {
418+
server.use(
419+
http.post(
420+
'https://api.clerk.test/v1/enterprise_connections/entconn_123/test_runs',
421+
validateHeaders(() => HttpResponse.json({ url: 'https://clerk.example.com/v1/test_runs/start?token=abc' })),
422+
),
423+
);
424+
425+
const response = await apiClient.enterpriseConnections.createEnterpriseConnectionTestRun('entconn_123');
426+
427+
expect(response).toEqual({ url: 'https://clerk.example.com/v1/test_runs/start?token=abc' });
428+
});
429+
430+
it('requires an enterprise connection ID', async () => {
431+
await expect(apiClient.enterpriseConnections.createEnterpriseConnectionTestRun('')).rejects.toThrow();
432+
});
433+
});
434+
435+
describe('getEnterpriseConnectionTestRunList', () => {
436+
const mockTestRunListResponse = {
437+
data: [
438+
{
439+
id: 'ectr_1',
440+
status: 'success',
441+
connection_type: 'saml',
442+
parsed_user_info: {
443+
email_address: 'jane@clerk.dev',
444+
first_name: 'Jane',
445+
last_name: 'Doe',
446+
user_id: 'jane',
447+
},
448+
logs: [
449+
{ level: 'info', code: 'response_received', short_message: 'Received', message: 'SAML response received' },
450+
],
451+
saml: {
452+
saml_request: 'PHNhbWxwOkF1dGhuUmVxdWVzdA==',
453+
saml_response: 'PHNhbWxwOlJlc3BvbnNl',
454+
relay_state: 'rs',
455+
},
456+
oauth: null,
457+
created_at: 1672531200000,
458+
},
459+
{
460+
id: 'ectr_2',
461+
status: 'failed',
462+
connection_type: 'oauth',
463+
parsed_user_info: null,
464+
oauth: { id_token: 'id', access_token: 'access', user_info: '{}' },
465+
created_at: 1672531100000,
466+
},
467+
],
468+
total_count: 2,
469+
};
470+
471+
it('fetches test runs as resources, with status filters and pagination in the query', async () => {
472+
let capturedRequestUrl: string | null = null;
473+
server.use(
474+
http.get(
475+
'https://api.clerk.test/v1/enterprise_connections/entconn_123/test_runs',
476+
validateHeaders(({ request }) => {
477+
capturedRequestUrl = request.url;
478+
return HttpResponse.json(mockTestRunListResponse);
479+
}),
480+
),
481+
);
482+
483+
const response = await apiClient.enterpriseConnections.getEnterpriseConnectionTestRunList('entconn_123', {
484+
status: ['success', 'failed'],
485+
limit: 10,
486+
offset: 5,
487+
});
488+
489+
expect(capturedRequestUrl).toBeTruthy();
490+
const url = new URL(capturedRequestUrl!);
491+
expect(url.searchParams.getAll('status')).toEqual(['success', 'failed']);
492+
expect(url.searchParams.get('limit')).toBe('10');
493+
expect(url.searchParams.get('offset')).toBe('5');
494+
495+
expect(response.totalCount).toBe(2);
496+
expect(response.data).toHaveLength(2);
497+
expectTypeOf(response.data).toEqualTypeOf<PublicEnterpriseConnectionTestRun[]>();
498+
499+
const [saml, oauth] = response.data;
500+
expect(saml).toBeInstanceOf(EnterpriseConnectionTestRun);
501+
expect(saml.status).toBe('success');
502+
expect(saml.connectionType).toBe('saml');
503+
expect(saml.createdAt).toBe(1672531200000);
504+
expect(saml.parsedUserInfo).toEqual({
505+
emailAddress: 'jane@clerk.dev',
506+
firstName: 'Jane',
507+
lastName: 'Doe',
508+
userId: 'jane',
509+
});
510+
expect(saml.logs).toEqual([
511+
{ level: 'info', code: 'response_received', shortMessage: 'Received', message: 'SAML response received' },
512+
]);
513+
expect(saml.saml?.samlResponse).toBe('PHNhbWxwOlJlc3BvbnNl');
514+
expect(saml.oauth).toBeNull();
515+
516+
expect(oauth.status).toBe('failed');
517+
expect(oauth.parsedUserInfo).toBeNull();
518+
expect(oauth.logs).toEqual([]);
519+
expect(oauth.saml).toBeNull();
520+
expect(oauth.oauth?.idToken).toBe('id');
521+
});
522+
523+
it('sends no query params when called without filters', async () => {
524+
let capturedRequestUrl: string | null = null;
525+
server.use(
526+
http.get(
527+
'https://api.clerk.test/v1/enterprise_connections/entconn_123/test_runs',
528+
validateHeaders(({ request }) => {
529+
capturedRequestUrl = request.url;
530+
return HttpResponse.json({ data: [], total_count: 0 });
531+
}),
532+
),
533+
);
534+
535+
const response = await apiClient.enterpriseConnections.getEnterpriseConnectionTestRunList('entconn_123');
536+
537+
expect(capturedRequestUrl).toBeTruthy();
538+
expect(new URL(capturedRequestUrl!).search).toBe('');
539+
expect(response).toEqual({ data: [], totalCount: 0 });
540+
});
541+
542+
it('requires an enterprise connection ID', async () => {
543+
await expect(apiClient.enterpriseConnections.getEnterpriseConnectionTestRunList('')).rejects.toThrow();
544+
});
545+
});
413546
});

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

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
import type { ClerkPaginationRequest, OrganizationEnterpriseConnectionProvider } from '@clerk/shared/types';
1+
import type {
2+
ClerkPaginationRequest,
3+
EnterpriseConnectionTestRunInitJSON,
4+
EnterpriseConnectionTestRunStatus,
5+
OrganizationEnterpriseConnectionProvider,
6+
} from '@clerk/shared/types';
27

38
import { joinPaths } from '../../util/path';
49
import type { EnterpriseConnection } from '../resources';
10+
import { EnterpriseConnectionTestRun } from '../resources';
511
import type { PaginatedResourceResponse } from '../resources/Deserializer';
12+
import type { EnterpriseConnectionTestRunJSON } from '../resources/JSON';
613
import { AbstractAPI } from './AbstractApi';
714

815
const basePath = '/enterprise_connections';
@@ -19,6 +26,14 @@ export type EnterpriseConnectionListParams = ClerkPaginationRequest<{
1926
active?: boolean;
2027
}>;
2128

29+
/** @generateWithEmptyComment */
30+
export type EnterpriseConnectionTestRunListParams = ClerkPaginationRequest<{
31+
/**
32+
* Filters test runs by status. If omitted, test runs of every status are returned.
33+
*/
34+
status?: EnterpriseConnectionTestRunStatus[];
35+
}>;
36+
2237
/** @inline */
2338
export interface EnterpriseConnectionOidcParams {
2439
/** The OAuth (OIDC) authorization URL. */
@@ -243,4 +258,56 @@ export class EnterpriseConnectionAPI extends AbstractAPI {
243258
path: joinPaths(basePath, enterpriseConnectionId),
244259
});
245260
}
261+
262+
/**
263+
* Starts a test run for the given enterprise connection: a test sign-in against its Identity Provider (IdP).
264+
* @param enterpriseConnectionId - The ID of the enterprise connection to test.
265+
* @returns An object with a `url` property: a short-lived URL that starts the test sign-in when visited.
266+
*
267+
* @example
268+
* ### Start a test run
269+
*
270+
* Send the person testing the connection to the returned `url` to sign in with the IdP.
271+
*
272+
* ```ts
273+
* const { url } = await clerkClient.enterpriseConnections.createEnterpriseConnectionTestRun('entconn_123');
274+
* ```
275+
*/
276+
public async createEnterpriseConnectionTestRun(enterpriseConnectionId: string) {
277+
this.requireId(enterpriseConnectionId);
278+
return this.request<EnterpriseConnectionTestRunInitJSON>({
279+
method: 'POST',
280+
path: joinPaths(basePath, enterpriseConnectionId, 'test_runs'),
281+
});
282+
}
283+
284+
/**
285+
* Gets the list of test runs for the given enterprise connection.
286+
* @param enterpriseConnectionId - The ID of the enterprise connection to get the test runs of.
287+
* @param params - Filters and pagination for the list.
288+
* @returns A [`PaginatedResourceResponse`](https://clerk.com/docs/reference/backend/types/paginated-resource-response) object with a `data` property containing an array of `EnterpriseConnectionTestRun` objects and a `totalCount` property containing the total number of test runs for the connection.
289+
*
290+
* @example
291+
* ### Get the failed test runs
292+
*
293+
* Filter by `status` to see why test sign-ins failed. Each test run's `logs` describe what happened.
294+
*
295+
* ```ts
296+
* const { data } = await clerkClient.enterpriseConnections.getEnterpriseConnectionTestRunList('entconn_123', {
297+
* status: ['failed'],
298+
* });
299+
* ```
300+
*/
301+
public async getEnterpriseConnectionTestRunList(
302+
enterpriseConnectionId: string,
303+
params: EnterpriseConnectionTestRunListParams = {},
304+
): Promise<PaginatedResourceResponse<EnterpriseConnectionTestRun[]>> {
305+
this.requireId(enterpriseConnectionId);
306+
const { data, totalCount } = await this.request<PaginatedResourceResponse<EnterpriseConnectionTestRunJSON[]>>({
307+
method: 'GET',
308+
path: joinPaths(basePath, enterpriseConnectionId, 'test_runs'),
309+
queryParams: params,
310+
});
311+
return { data: data.map(testRun => EnterpriseConnectionTestRun.fromJSON(testRun)), totalCount };
312+
}
246313
}

0 commit comments

Comments
 (0)