1+ import { ClerkAPIResponseError } from '@clerk/shared/error' ;
12import { beforeEach , describe , expect , it , vi } from 'vitest' ;
23
34import { Flow } from '@/customizables' ;
45import { bindCreateFixtures } from '@/test/create-fixtures' ;
56import { render , screen } from '@/test/utils' ;
6- import { CardStateProvider } from '@/ui/elements/contexts' ;
7+ import { CardStateProvider , useCardState } from '@/ui/elements/contexts' ;
78
89import type { EnterpriseConnectionProviderType } from '../../../types' ;
910
@@ -12,6 +13,7 @@ import type { EnterpriseConnectionProviderType } from '../../../types';
1213// left undefined so that footer self-hides in this isolated render.
1314const contextState = vi . hoisted ( ( ) => ( {
1415 provider : undefined as string | undefined ,
16+ isOIDCFlowEnabled : true ,
1517 enterpriseConnection : undefined as
1618 | {
1719 id : string ;
@@ -27,6 +29,12 @@ const contextState = vi.hoisted(() => ({
2729} ) ) ;
2830const updateConnection = vi . hoisted ( ( ) => vi . fn ( ) ) ;
2931
32+ const CardErrorProbe = ( ) => {
33+ const { error } = useCardState ( ) ;
34+
35+ return error ? < div > { error } </ div > : null ;
36+ } ;
37+
3038vi . mock ( '../../../ConfigureSSOContext' , ( ) => ( {
3139 useConfigureSSO : ( ) => ( {
3240 enterpriseConnection : contextState . enterpriseConnection ,
@@ -36,6 +44,7 @@ vi.mock('../../../ConfigureSSOContext', () => ({
3644 provider : contextState . provider ,
3745 hasConnection : true ,
3846 } ,
47+ isOIDCFlowEnabled : contextState . isOIDCFlowEnabled ,
3948 } ) ,
4049} ) ) ;
4150
@@ -52,28 +61,34 @@ const { createFixtures } = bindCreateFixtures('ConfigureSSO');
5261
5362describe ( 'resolveConfigureSteps' , ( ) => {
5463 it ( 'dispatches custom and legacy OIDC provider keys to the OIDC sub-flow' , ( ) => {
55- expect ( resolveConfigureSteps ( 'oauth_custom_clerk_dev' ) ) . toBe ( OidcCustomConfigureSteps ) ;
56- expect ( resolveConfigureSteps ( 'oidc_clerk_dev' ) ) . toBe ( OidcCustomConfigureSteps ) ;
57- expect ( resolveConfigureSteps ( 'oidc_ghe_acme' ) ) . toBe ( OidcCustomConfigureSteps ) ;
58- expect ( resolveConfigureSteps ( 'oidc_gitlab_ent_acme' ) ) . toBe ( OidcCustomConfigureSteps ) ;
59- expect ( resolveConfigureSteps ( 'oidc_custom' ) ) . toBe ( OidcCustomConfigureSteps ) ;
64+ expect ( resolveConfigureSteps ( 'oauth_custom_clerk_dev' , true ) ) . toBe ( OidcCustomConfigureSteps ) ;
65+ expect ( resolveConfigureSteps ( 'oidc_clerk_dev' , true ) ) . toBe ( OidcCustomConfigureSteps ) ;
66+ expect ( resolveConfigureSteps ( 'oidc_ghe_acme' , true ) ) . toBe ( OidcCustomConfigureSteps ) ;
67+ expect ( resolveConfigureSteps ( 'oidc_gitlab_ent_acme' , true ) ) . toBe ( OidcCustomConfigureSteps ) ;
68+ expect ( resolveConfigureSteps ( 'oidc_custom' , true ) ) . toBe ( OidcCustomConfigureSteps ) ;
69+ } ) ;
70+
71+ it ( 'does not dispatch OIDC providers while the experimental flow is disabled' , ( ) => {
72+ expect ( resolveConfigureSteps ( 'oauth_custom_clerk_dev' , false ) ) . toBeUndefined ( ) ;
73+ expect ( resolveConfigureSteps ( 'oidc_clerk_dev' , false ) ) . toBeUndefined ( ) ;
6074 } ) ;
6175
6276 it ( 'dispatches SAML providers by exact literal' , ( ) => {
63- expect ( resolveConfigureSteps ( 'saml_okta' ) ) . toBe ( SamlOktaConfigureSteps ) ;
64- expect ( resolveConfigureSteps ( 'saml_custom' ) ) . toBe ( SamlCustomConfigureSteps ) ;
65- expect ( resolveConfigureSteps ( 'saml_google' ) ) . toBe ( SamlGoogleConfigureSteps ) ;
66- expect ( resolveConfigureSteps ( 'saml_microsoft' ) ) . toBe ( SamlMicrosoftConfigureSteps ) ;
77+ expect ( resolveConfigureSteps ( 'saml_okta' , false ) ) . toBe ( SamlOktaConfigureSteps ) ;
78+ expect ( resolveConfigureSteps ( 'saml_custom' , false ) ) . toBe ( SamlCustomConfigureSteps ) ;
79+ expect ( resolveConfigureSteps ( 'saml_google' , false ) ) . toBe ( SamlGoogleConfigureSteps ) ;
80+ expect ( resolveConfigureSteps ( 'saml_microsoft' , false ) ) . toBe ( SamlMicrosoftConfigureSteps ) ;
6781 } ) ;
6882
6983 it ( 'returns undefined for an unrecognized provider so the caller can degrade' , ( ) => {
70- expect ( resolveConfigureSteps ( 'ldap_enterprise' as EnterpriseConnectionProviderType ) ) . toBeUndefined ( ) ;
84+ expect ( resolveConfigureSteps ( 'ldap_enterprise' as EnterpriseConnectionProviderType , true ) ) . toBeUndefined ( ) ;
7185 } ) ;
7286} ) ;
7387
7488describe ( 'ConfigureProviderStep' , ( ) => {
7589 beforeEach ( ( ) => {
7690 contextState . provider = undefined ;
91+ contextState . isOIDCFlowEnabled = true ;
7792 contextState . enterpriseConnection = undefined ;
7893 updateConnection . mockReset ( ) ;
7994 } ) ;
@@ -83,6 +98,7 @@ describe('ConfigureProviderStep', () => {
8398 < Flow . Root flow = 'configureSSO' >
8499 < CardStateProvider >
85100 < ConfigureProviderStep />
101+ < CardErrorProbe />
86102 </ CardStateProvider >
87103 </ Flow . Root > ,
88104 { wrapper } ,
@@ -211,6 +227,36 @@ describe('ConfigureProviderStep', () => {
211227 } ) ;
212228 } ) ;
213229
230+ it ( 'clears endpoint API errors when switching configuration modes' , async ( ) => {
231+ contextState . provider = 'oidc_clerk_dev' ;
232+ contextState . enterpriseConnection = { id : 'ent_123' , oauthConfig : null } ;
233+ updateConnection . mockRejectedValueOnce (
234+ new ClerkAPIResponseError ( 'Error' , {
235+ data : [
236+ {
237+ code : 'form_param_invalid' ,
238+ long_message : 'The endpoint configuration is invalid.' ,
239+ message : 'The endpoint configuration is invalid.' ,
240+ } ,
241+ ] ,
242+ status : 422 ,
243+ } ) ,
244+ ) ;
245+ const { wrapper } = await createFixtures ( ) ;
246+
247+ const { userEvent } = renderStep ( wrapper ) ;
248+
249+ await userEvent . click ( await screen . findByRole ( 'button' , { name : 'Continue' } ) ) ;
250+ await userEvent . type ( screen . getByRole ( 'textbox' , { name : 'Discovery endpoint' } ) , 'https://idp.example/discovery' ) ;
251+ await userEvent . click ( screen . getByRole ( 'button' , { name : 'Continue' } ) ) ;
252+
253+ expect ( await screen . findByText ( 'The endpoint configuration is invalid.' ) ) . toBeInTheDocument ( ) ;
254+
255+ await userEvent . click ( screen . getByRole ( 'radio' , { name : 'Configure manually' } ) ) ;
256+
257+ expect ( screen . queryByText ( 'The endpoint configuration is invalid.' ) ) . not . toBeInTheDocument ( ) ;
258+ } ) ;
259+
214260 it ( 'saves credentials before advancing' , async ( ) => {
215261 contextState . provider = 'oidc_clerk_dev' ;
216262 contextState . enterpriseConnection = { id : 'ent_123' , oauthConfig : null } ;
@@ -241,6 +287,53 @@ describe('ConfigureProviderStep', () => {
241287 } ) ;
242288 } ) ;
243289
290+ it ( 'displays credential API errors on their matching fields' , async ( ) => {
291+ contextState . provider = 'oidc_clerk_dev' ;
292+ contextState . enterpriseConnection = { id : 'ent_123' , oauthConfig : null } ;
293+ updateConnection . mockReset ( ) ;
294+ updateConnection . mockResolvedValueOnce ( { } ) ;
295+ updateConnection . mockRejectedValueOnce (
296+ new ClerkAPIResponseError ( 'Error' , {
297+ data : [
298+ {
299+ code : 'form_param_invalid' ,
300+ long_message : 'Client ID is invalid.' ,
301+ message : 'Client ID is invalid.' ,
302+ meta : { param_name : 'client_id' } ,
303+ } ,
304+ {
305+ code : 'form_param_invalid' ,
306+ long_message : 'Client secret is invalid.' ,
307+ message : 'Client secret is invalid.' ,
308+ meta : { param_name : 'client_secret' } ,
309+ } ,
310+ ] ,
311+ status : 422 ,
312+ } ) ,
313+ ) ;
314+ const { wrapper } = await createFixtures ( ) ;
315+
316+ const { userEvent } = renderStep ( wrapper ) ;
317+
318+ await userEvent . click ( await screen . findByRole ( 'button' , { name : 'Continue' } ) ) ;
319+ await userEvent . type (
320+ screen . getByRole ( 'textbox' , { name : 'Discovery endpoint' } ) ,
321+ 'https://idp.example/.well-known/openid-configuration' ,
322+ ) ;
323+ await userEvent . click ( screen . getByRole ( 'button' , { name : 'Continue' } ) ) ;
324+
325+ const clientId = await screen . findByRole ( 'textbox' , { name : 'Client ID' } ) ;
326+ const clientSecret = screen . getByLabelText ( 'Client secret' ) ;
327+ await userEvent . type ( clientId , 'client_123' ) ;
328+ await userEvent . type ( clientSecret , 'secret_456' ) ;
329+ await userEvent . click ( screen . getByRole ( 'button' , { name : 'Continue' } ) ) ;
330+
331+ expect ( await screen . findByText ( 'Client ID is invalid.' ) ) . toBeInTheDocument ( ) ;
332+ expect ( await screen . findByText ( 'Client secret is invalid.' ) ) . toBeInTheDocument ( ) ;
333+ expect ( clientId ) . toHaveAttribute ( 'aria-describedby' , 'error-clientId' ) ;
334+ expect ( clientSecret ) . toHaveAttribute ( 'aria-describedby' , 'error-clientSecret' ) ;
335+ } ) ;
336+
244337 it ( 'selects manual mode when an existing connection has manual endpoints without a discovery URL' , async ( ) => {
245338 contextState . provider = 'oidc_clerk_dev' ;
246339 contextState . enterpriseConnection = {
@@ -324,4 +417,15 @@ describe('ConfigureProviderStep', () => {
324417
325418 expect ( await screen . findByText ( / u n s u p p o r t e d p r o v i d e r / i) ) . toBeInTheDocument ( ) ;
326419 } ) ;
420+
421+ it ( 'degrades to the unsupported-provider state for an existing OIDC connection when the flag is off' , async ( ) => {
422+ contextState . provider = 'oauth_custom_clerk_dev' ;
423+ contextState . isOIDCFlowEnabled = false ;
424+ const { wrapper } = await createFixtures ( ) ;
425+
426+ renderStep ( wrapper ) ;
427+
428+ expect ( await screen . findByText ( / u n s u p p o r t e d p r o v i d e r / i) ) . toBeInTheDocument ( ) ;
429+ expect ( screen . queryByText ( / c r e a t e a n e w o i d c a p p l i c a t i o n / i) ) . not . toBeInTheDocument ( ) ;
430+ } ) ;
327431} ) ;
0 commit comments