@@ -74,17 +74,6 @@ private static IAuthenticationService SetupHttpContextWithAuth(
7474 return resolvedAuthService ;
7575 }
7676
77- private static void InvokeEnsureOrgUserStatusAllowed (
78- AccountController controller ,
79- OrganizationUserStatusType status )
80- {
81- var method = typeof ( AccountController ) . GetMethod (
82- "EnsureAcceptedOrConfirmedOrgUserStatus" ,
83- BindingFlags . Instance | BindingFlags . NonPublic ) ;
84- Assert . NotNull ( method ) ;
85- method . Invoke ( controller , [ status , "Org" ] ) ;
86- }
87-
8877 private static AuthenticateResult BuildSuccessfulExternalAuth ( Guid orgId , string providerUserId , string email )
8978 {
9079 var claims = new [ ]
@@ -241,82 +230,6 @@ private async Task<LookupCounts> MeasureCountsForScenarioAsync(
241230 return counts ;
242231 }
243232
244- [ Theory , BitAutoData ]
245- public void EnsureOrgUserStatusAllowed_AllowsAcceptedAndConfirmed (
246- SutProvider < AccountController > sutProvider )
247- {
248- // Arrange
249- sutProvider . GetDependency < II18nService > ( )
250- . T ( Arg . Any < string > ( ) , Arg . Any < object ? [ ] > ( ) )
251- . Returns ( ci => ( string ) ci [ 0 ] ! ) ;
252-
253- // Act
254- var ex1 = Record . Exception ( ( ) =>
255- InvokeEnsureOrgUserStatusAllowed ( sutProvider . Sut , OrganizationUserStatusType . Accepted ) ) ;
256- var ex2 = Record . Exception ( ( ) =>
257- InvokeEnsureOrgUserStatusAllowed ( sutProvider . Sut , OrganizationUserStatusType . Confirmed ) ) ;
258-
259- // Assert
260- Assert . Null ( ex1 ) ;
261- Assert . Null ( ex2 ) ;
262- }
263-
264- [ Theory , BitAutoData ]
265- public void EnsureOrgUserStatusAllowed_Invited_ThrowsAcceptInvite (
266- SutProvider < AccountController > sutProvider )
267- {
268- // Arrange
269- sutProvider . GetDependency < II18nService > ( )
270- . T ( Arg . Any < string > ( ) , Arg . Any < object ? [ ] > ( ) )
271- . Returns ( ci => ( string ) ci [ 0 ] ! ) ;
272-
273- // Act
274- var ex = Assert . Throws < TargetInvocationException > ( ( ) =>
275- InvokeEnsureOrgUserStatusAllowed ( sutProvider . Sut , OrganizationUserStatusType . Invited ) ) ;
276-
277- // Assert
278- Assert . IsType < Exception > ( ex . InnerException ) ;
279- Assert . Equal ( "AcceptInviteBeforeUsingSSO" , ex . InnerException ! . Message ) ;
280- }
281-
282- [ Theory , BitAutoData ]
283- public void EnsureOrgUserStatusAllowed_Revoked_ThrowsAccessRevoked (
284- SutProvider < AccountController > sutProvider )
285- {
286- // Arrange
287- sutProvider . GetDependency < II18nService > ( )
288- . T ( Arg . Any < string > ( ) , Arg . Any < object ? [ ] > ( ) )
289- . Returns ( ci => ( string ) ci [ 0 ] ! ) ;
290-
291- // Act
292- var ex = Assert . Throws < TargetInvocationException > ( ( ) =>
293- InvokeEnsureOrgUserStatusAllowed ( sutProvider . Sut , OrganizationUserStatusType . Revoked ) ) ;
294-
295- // Assert
296- Assert . IsType < Exception > ( ex . InnerException ) ;
297- Assert . Equal ( "OrganizationUserAccessRevoked" , ex . InnerException ! . Message ) ;
298- }
299-
300- [ Theory , BitAutoData ]
301- public void EnsureOrgUserStatusAllowed_UnknownStatus_ThrowsUnknown (
302- SutProvider < AccountController > sutProvider )
303- {
304- // Arrange
305- sutProvider . GetDependency < II18nService > ( )
306- . T ( Arg . Any < string > ( ) , Arg . Any < object ? [ ] > ( ) )
307- . Returns ( ci => ( string ) ci [ 0 ] ! ) ;
308-
309- var unknown = ( OrganizationUserStatusType ) 999 ;
310-
311- // Act
312- var ex = Assert . Throws < TargetInvocationException > ( ( ) =>
313- InvokeEnsureOrgUserStatusAllowed ( sutProvider . Sut , unknown ) ) ;
314-
315- // Assert
316- Assert . IsType < Exception > ( ex . InnerException ) ;
317- Assert . Equal ( "OrganizationUserUnknownStatus" , ex . InnerException ! . Message ) ;
318- }
319-
320233 [ Theory , BitAutoData ]
321234 public async Task ExternalCallback_PreventNonCompliantTrue_ExistingUser_NoOrgUser_ThrowsCouldNotFindOrganizationUser (
322235 SutProvider < AccountController > sutProvider )
@@ -357,7 +270,7 @@ public async Task ExternalCallback_PreventNonCompliantTrue_ExistingUser_NoOrgUse
357270 }
358271
359272 [ Theory , BitAutoData ]
360- public async Task ExternalCallback_PreventNonCompliantTrue_ExistingUser_OrgUserInvited_ThrowsAcceptInvite (
273+ public async Task ExternalCallback_PreventNonCompliantTrue_ExistingUser_OrgUserInvited_AllowsLogin (
361274 SutProvider < AccountController > sutProvider )
362275 {
363276 // Arrange
@@ -374,7 +287,7 @@ public async Task ExternalCallback_PreventNonCompliantTrue_ExistingUser_OrgUserI
374287 } ;
375288
376289 var authResult = BuildSuccessfulExternalAuth ( orgId , providerUserId , user . Email ! ) ;
377- SetupHttpContextWithAuth ( sutProvider , authResult ) ;
290+ var authService = SetupHttpContextWithAuth ( sutProvider , authResult ) ;
378291
379292 sutProvider . GetDependency < II18nService > ( )
380293 . T ( Arg . Any < string > ( ) , Arg . Any < object ? [ ] > ( ) )
@@ -392,9 +305,23 @@ public async Task ExternalCallback_PreventNonCompliantTrue_ExistingUser_OrgUserI
392305 sutProvider . GetDependency < IIdentityServerInteractionService > ( )
393306 . GetAuthorizationContextAsync ( "~/" ) . Returns ( ( AuthorizationRequest ? ) null ) ;
394307
395- // Act + Assert
396- var ex = await Assert . ThrowsAsync < Exception > ( ( ) => sutProvider . Sut . ExternalCallback ( ) ) ;
397- Assert . Equal ( "AcceptInviteBeforeUsingSSO" , ex . Message ) ;
308+ // Act
309+ var result = await sutProvider . Sut . ExternalCallback ( ) ;
310+
311+ // Assert
312+ var redirect = Assert . IsType < RedirectResult > ( result ) ;
313+ Assert . Equal ( "~/" , redirect . Url ) ;
314+
315+ await authService . Received ( ) . SignInAsync (
316+ Arg . Any < HttpContext > ( ) ,
317+ Arg . Any < string ? > ( ) ,
318+ Arg . Any < ClaimsPrincipal > ( ) ,
319+ Arg . Any < AuthenticationProperties > ( ) ) ;
320+
321+ await authService . Received ( ) . SignOutAsync (
322+ Arg . Any < HttpContext > ( ) ,
323+ AuthenticationSchemes . BitwardenExternalCookieAuthenticationScheme ,
324+ Arg . Any < AuthenticationProperties > ( ) ) ;
398325 }
399326
400327 [ Theory , BitAutoData ]
@@ -930,13 +857,13 @@ public async Task ExternalCallback_PreventNonCompliantFalse_JitProvision_Measure
930857 }
931858
932859 [ Theory , BitAutoData ]
933- public async Task AutoProvisionUserAsync_WithExistingAcceptedUser_CreatesSsoLinkAndReturnsUser (
860+ public async Task CreateUserAndOrgUserConditionallyAsync_WithExistingAcceptedUser_CreatesSsoLinkAndReturnsUser (
934861 SutProvider < AccountController > sutProvider )
935862 {
936863 // Arrange
937864 var orgId = Guid . NewGuid ( ) ;
938- var providerUserId = "ext-456 " ;
939- var email = "jit @example.com" ;
865+ var providerUserId = "provider-user-id " ;
866+ var email = "user @example.com" ;
940867 var existingUser = new User { Id = Guid . NewGuid ( ) , Email = email } ;
941868 var organization = new Organization { Id = orgId , Name = "Org" } ;
942869 var orgUser = new OrganizationUser
@@ -965,12 +892,12 @@ public async Task AutoProvisionUserAsync_WithExistingAcceptedUser_CreatesSsoLink
965892 var config = new SsoConfigurationData ( ) ;
966893
967894 var method = typeof ( AccountController ) . GetMethod (
968- "AutoProvisionUserAsync " ,
895+ "CreateUserAndOrgUserConditionallyAsync " ,
969896 BindingFlags . Instance | BindingFlags . NonPublic ) ;
970897 Assert . NotNull ( method ) ;
971898
972899 // Act
973- var task = ( Task < ( User user , Organization organization , OrganizationUser orgUser ) > ) method ! . Invoke ( sutProvider . Sut , new object [ ]
900+ var task = ( Task < ( User user , Organization organization , OrganizationUser orgUser ) > ) method . Invoke ( sutProvider . Sut , new object [ ]
974901 {
975902 orgId . ToString ( ) ,
976903 providerUserId ,
@@ -992,6 +919,61 @@ await sutProvider.GetDependency<ISsoUserRepository>().Received().CreateAsync(Arg
992919 EventType . OrganizationUser_FirstSsoLogin ) ;
993920 }
994921
922+ [ Theory , BitAutoData ]
923+ public async Task CreateUserAndOrgUserConditionallyAsync_WithExistingInvitedUser_ThrowsAcceptInviteBeforeUsingSSO (
924+ SutProvider < AccountController > sutProvider )
925+ {
926+ // Arrange
927+ var orgId = Guid . NewGuid ( ) ;
928+ var providerUserId = "provider-user-id" ;
929+ 930+ var existingUser = new User { Id = Guid . NewGuid ( ) , Email = email , UsesKeyConnector = false } ;
931+ var organization = new Organization { Id = orgId , Name = "Org" } ;
932+ var orgUser = new OrganizationUser
933+ {
934+ OrganizationId = orgId ,
935+ UserId = existingUser . Id ,
936+ Status = OrganizationUserStatusType . Invited ,
937+ Type = OrganizationUserType . User
938+ } ;
939+
940+ // i18n returns the key so we can assert on message contents
941+ sutProvider . GetDependency < II18nService > ( )
942+ . T ( Arg . Any < string > ( ) , Arg . Any < object ? [ ] > ( ) )
943+ . Returns ( ci => ( string ) ci [ 0 ] ! ) ;
944+
945+ // Arrange repository expectations for the flow
946+ sutProvider . GetDependency < IUserRepository > ( ) . GetByEmailAsync ( email ) . Returns ( existingUser ) ;
947+ sutProvider . GetDependency < IOrganizationRepository > ( ) . GetByIdAsync ( orgId ) . Returns ( organization ) ;
948+ sutProvider . GetDependency < IOrganizationUserRepository > ( ) . GetManyByUserAsync ( existingUser . Id )
949+ . Returns ( new List < OrganizationUser > { orgUser } ) ;
950+
951+ var claims = new [ ]
952+ {
953+ new Claim ( JwtClaimTypes . Email , email ) ,
954+ new Claim ( JwtClaimTypes . Name , "Invited User" )
955+ } as IEnumerable < Claim > ;
956+ var config = new SsoConfigurationData ( ) ;
957+
958+ var method = typeof ( AccountController ) . GetMethod (
959+ "CreateUserAndOrgUserConditionallyAsync" ,
960+ BindingFlags . Instance | BindingFlags . NonPublic ) ;
961+ Assert . NotNull ( method ) ;
962+
963+ // Act + Assert
964+ var task = ( Task < ( User user , Organization organization , OrganizationUser orgUser ) > ) method . Invoke ( sutProvider . Sut , new object [ ]
965+ {
966+ orgId . ToString ( ) ,
967+ providerUserId ,
968+ claims ,
969+ null ! ,
970+ config
971+ } ) ! ;
972+
973+ var ex = await Assert . ThrowsAsync < Exception > ( async ( ) => await task ) ;
974+ Assert . Equal ( "AcceptInviteBeforeUsingSSO" , ex . Message ) ;
975+ }
976+
995977 /// <summary>
996978 /// PM-24579: Temporary comparison test to ensure the feature flag ON does not
997979 /// regress lookup counts compared to OFF. When removing the flag, delete this
0 commit comments