1717using System . Linq ;
1818using System . Net . Http ;
1919using System . Text ;
20+ using System . Threading ;
2021using System . Threading . Tasks ;
2122using System . Web ;
2223using FirebaseAdmin . Auth ;
@@ -37,6 +38,9 @@ public class FirebaseAuthTest
3738 private const string VerifyCustomTokenUrl =
3839 "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken" ;
3940
41+ private const string VerifyPasswordUrl =
42+ "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword" ;
43+
4044 private const string ContinueUrl = "http://localhost/?a=1&b=2#c=3" ;
4145
4246 private static readonly ActionCodeSettings EmailLinkSettings = new ActionCodeSettings ( )
@@ -336,6 +340,37 @@ public async Task GetUserNonExistingEmail()
336340 Assert . Equal ( AuthErrorCode . UserNotFound , exception . AuthErrorCode ) ;
337341 }
338342
343+ [ Fact ]
344+ public async Task LastRefreshTime ( )
345+ {
346+ var newUserRecord = await NewUserWithParamsAsync ( ) ;
347+ try
348+ {
349+ // New users should not have a LastRefreshTimestamp set.
350+ Assert . Null ( newUserRecord . UserMetaData . LastRefreshTimestamp ) ;
351+
352+ // Login to cause the LastRefreshTimestamp to be set.
353+ await SignInWithPasswordAsync ( newUserRecord . Email , "password" ) ;
354+
355+ var userRecord = await FirebaseAuth . DefaultInstance . GetUserAsync ( newUserRecord . Uid ) ;
356+
357+ // Ensure the LastRefreshTimstamp is approximately "now" (with a tollerance of 10 minutes).
358+ var now = DateTime . UtcNow ;
359+ int tolleranceMinutes = 10 ;
360+ var minTime = now . AddMinutes ( - tolleranceMinutes ) ;
361+ var maxTime = now . AddMinutes ( tolleranceMinutes ) ;
362+ Assert . NotNull ( userRecord . UserMetaData . LastRefreshTimestamp ) ;
363+ Assert . InRange (
364+ userRecord . UserMetaData . LastRefreshTimestamp . Value ,
365+ minTime ,
366+ maxTime ) ;
367+ }
368+ finally
369+ {
370+ await FirebaseAuth . DefaultInstance . DeleteUserAsync ( newUserRecord . Uid ) ;
371+ }
372+ }
373+
339374 [ Fact ]
340375 public async Task UpdateUserNonExistingUid ( )
341376 {
@@ -359,6 +394,76 @@ public async Task DeleteUserNonExistingUid()
359394 Assert . Equal ( AuthErrorCode . UserNotFound , exception . AuthErrorCode ) ;
360395 }
361396
397+ [ Fact ]
398+ public async Task DeleteUsers ( )
399+ {
400+ UserRecord user1 = await NewUserWithParamsAsync ( ) ;
401+ UserRecord user2 = await NewUserWithParamsAsync ( ) ;
402+ UserRecord user3 = await NewUserWithParamsAsync ( ) ;
403+
404+ DeleteUsersResult deleteUsersResult = await this . SlowDeleteUsersAsync (
405+ new List < string > { user1 . Uid , user2 . Uid , user3 . Uid } ) ;
406+
407+ Assert . Equal ( 3 , deleteUsersResult . SuccessCount ) ;
408+ Assert . Equal ( 0 , deleteUsersResult . FailureCount ) ;
409+ Assert . Empty ( deleteUsersResult . Errors ) ;
410+
411+ GetUsersResult getUsersResult = await FirebaseAuth . DefaultInstance . GetUsersAsync (
412+ new List < UserIdentifier >
413+ {
414+ new UidIdentifier ( user1 . Uid ) ,
415+ new UidIdentifier ( user2 . Uid ) ,
416+ new UidIdentifier ( user3 . Uid ) ,
417+ } ) ;
418+
419+ Assert . Empty ( getUsersResult . Users ) ;
420+ Assert . Equal ( 3 , getUsersResult . NotFound . Count ( ) ) ;
421+ }
422+
423+ [ Fact ]
424+ public async Task DeleteExistingAndNonExistingUsers ( )
425+ {
426+ UserRecord user1 = await NewUserWithParamsAsync ( ) ;
427+
428+ DeleteUsersResult deleteUsersResult = await this . SlowDeleteUsersAsync (
429+ new List < string > { user1 . Uid , "uid-that-doesnt-exist" } ) ;
430+
431+ Assert . Equal ( 2 , deleteUsersResult . SuccessCount ) ;
432+ Assert . Equal ( 0 , deleteUsersResult . FailureCount ) ;
433+ Assert . Empty ( deleteUsersResult . Errors ) ;
434+
435+ GetUsersResult getUsersResult = await FirebaseAuth . DefaultInstance . GetUsersAsync (
436+ new List < UserIdentifier >
437+ {
438+ new UidIdentifier ( user1 . Uid ) ,
439+ new UidIdentifier ( "uid-that-doesnt-exist" ) ,
440+ } ) ;
441+
442+ Assert . Empty ( getUsersResult . Users ) ;
443+ Assert . Equal ( 2 , getUsersResult . NotFound . Count ( ) ) ;
444+ }
445+
446+ [ Fact ]
447+ public async Task DeleteUsersIsIdempotent ( )
448+ {
449+ UserRecord user1 = await NewUserWithParamsAsync ( ) ;
450+
451+ DeleteUsersResult result = await this . SlowDeleteUsersAsync (
452+ new List < string > { user1 . Uid } ) ;
453+
454+ Assert . Equal ( 1 , result . SuccessCount ) ;
455+ Assert . Equal ( 0 , result . FailureCount ) ;
456+ Assert . Empty ( result . Errors ) ;
457+
458+ // Delete the user again, ensuring that everything still counts as a success.
459+ result = await this . SlowDeleteUsersAsync (
460+ new List < string > { user1 . Uid } ) ;
461+
462+ Assert . Equal ( 1 , result . SuccessCount ) ;
463+ Assert . Equal ( 0 , result . FailureCount ) ;
464+ Assert . Empty ( result . Errors ) ;
465+ }
466+
362467 [ Fact ]
363468 public async Task ListUsers ( )
364469 {
@@ -520,6 +625,24 @@ public async Task SessionCookie()
520625 Assert . Equal ( "testuser" , decoded . Uid ) ;
521626 }
522627
628+ private static async Task < UserRecord > NewUserWithParamsAsync ( )
629+ {
630+ // TODO(rsgowman): This function could be used throughout this file
631+ // (similar to the other ports).
632+ RandomUser randomUser = RandomUser . Create ( ) ;
633+ var args = new UserRecordArgs ( )
634+ {
635+ Uid = randomUser . Uid ,
636+ Email = randomUser . Email ,
637+ PhoneNumber = randomUser . PhoneNumber ,
638+ DisplayName = "Random User" ,
639+ PhotoUrl = "https://example.com/photo.png" ,
640+ Password = "password" ,
641+ } ;
642+
643+ return await FirebaseAuth . DefaultInstance . CreateUserAsync ( args ) ;
644+ }
645+
523646 private static async Task < UserRecord > CreateUserForActionLinksAsync ( )
524647 {
525648 var randomUser = RandomUser . Create ( ) ;
@@ -558,6 +681,33 @@ private static async Task<string> SignInWithCustomTokenAsync(string customToken)
558681 }
559682 }
560683
684+ private static async Task < string > SignInWithPasswordAsync ( string email , string password )
685+ {
686+ var rb = new Google . Apis . Requests . RequestBuilder ( )
687+ {
688+ Method = Google . Apis . Http . HttpConsts . Post ,
689+ BaseUri = new Uri ( VerifyPasswordUrl ) ,
690+ } ;
691+ rb . AddParameter ( RequestParameterType . Query , "key" , IntegrationTestUtils . GetApiKey ( ) ) ;
692+ var request = rb . CreateRequest ( ) ;
693+ var jsonSerializer = Google . Apis . Json . NewtonsoftJsonSerializer . Instance ;
694+ var payload = jsonSerializer . Serialize ( new VerifyPasswordRequest
695+ {
696+ Email = email ,
697+ Password = password ,
698+ ReturnSecureToken = true ,
699+ } ) ;
700+ request . Content = new StringContent ( payload , Encoding . UTF8 , "application/json" ) ;
701+ using ( var client = new HttpClient ( ) )
702+ {
703+ var response = await client . SendAsync ( request ) ;
704+ response . EnsureSuccessStatusCode ( ) ;
705+ var json = await response . Content . ReadAsStringAsync ( ) ;
706+ var parsed = jsonSerializer . Deserialize < VerifyPasswordResponse > ( json ) ;
707+ return parsed . IdToken ;
708+ }
709+ }
710+
561711 private static async Task < string > SignInWithEmailLinkAsync ( string email , string oobCode )
562712 {
563713 var rb = new Google . Apis . Requests . RequestBuilder ( )
@@ -610,6 +760,132 @@ private static async Task<string> ResetPasswordAsync(ResetPasswordRequest data)
610760 return ( string ) parsed [ "email" ] ;
611761 }
612762 }
763+
764+ /**
765+ * The {@code batchDelete} endpoint is currently rate limited to 1qps. Use this test helper
766+ * to ensure you don't run into quota exceeded errors.
767+ */
768+ // TODO(rsgowman): When/if the rate limit is relaxed, eliminate this helper.
769+ private async Task < DeleteUsersResult > SlowDeleteUsersAsync ( IReadOnlyList < string > uids )
770+ {
771+ await Task . Delay ( millisecondsDelay : 1000 ) ;
772+ return await FirebaseAuth . DefaultInstance . DeleteUsersAsync ( uids ) ;
773+ }
774+
775+ public class GetUsersFixture : IDisposable
776+ {
777+ public GetUsersFixture ( )
778+ {
779+ IntegrationTestUtils . EnsureDefaultApp ( ) ;
780+
781+ this . TestUser1 = NewUserWithParamsAsync ( ) . Result ;
782+ this . TestUser2 = NewUserWithParamsAsync ( ) . Result ;
783+ this . TestUser3 = NewUserWithParamsAsync ( ) . Result ;
784+
785+ // The C# port doesn't support importing users, so unlike the other ports, there's
786+ // no way to create a user with a linked federated provider.
787+ // TODO(rsgowman): Once either FirebaseAuth.ImportUser() exists (or the UpdateUser()
788+ // method supports ProviderToLink (#143)), then use it here and
789+ // adjust the VariousIdentifiers() test below.
790+ }
791+
792+ public UserRecord TestUser1 { get ; }
793+
794+ public UserRecord TestUser2 { get ; }
795+
796+ public UserRecord TestUser3 { get ; }
797+
798+ public void Dispose ( )
799+ {
800+ // TODO(rsgowman): deleteUsers (plural) would make more sense here, but it's
801+ // currently rate limited to 1qps. When/if that's relaxed, change this to just
802+ // delete them all at once.
803+ var auth = FirebaseAuth . DefaultInstance ;
804+ auth . DeleteUserAsync ( this . TestUser1 . Uid ) . Wait ( ) ;
805+ auth . DeleteUserAsync ( this . TestUser2 . Uid ) . Wait ( ) ;
806+ auth . DeleteUserAsync ( this . TestUser3 . Uid ) . Wait ( ) ;
807+ }
808+ }
809+
810+ public class GetUsers : IClassFixture < GetUsersFixture >
811+ {
812+ private FirebaseAuth auth ;
813+ private UserRecord testUser1 ;
814+ private UserRecord testUser2 ;
815+ private UserRecord testUser3 ;
816+
817+ public GetUsers ( GetUsersFixture fixture )
818+ {
819+ this . auth = FirebaseAuth . DefaultInstance ;
820+ this . testUser1 = fixture . TestUser1 ;
821+ this . testUser2 = fixture . TestUser2 ;
822+ this . testUser3 = fixture . TestUser3 ;
823+ }
824+
825+ [ Fact ]
826+ public async void VariousIdentifiers ( )
827+ {
828+ var getUsersResult = await this . auth . GetUsersAsync ( new List < UserIdentifier > ( )
829+ {
830+ new UidIdentifier ( this . testUser1 . Uid ) ,
831+ new EmailIdentifier ( this . testUser2 . Email ) ,
832+ new PhoneIdentifier ( this . testUser3 . PhoneNumber ) ,
833+ // TODO(rsgowman): Once we're able to create a user with a
834+ // provider, do so above and fetch the user like this:
835+ // new ProviderIdentifier("google.com", "google_" + importUserUid),
836+ } ) ;
837+
838+ var uids = getUsersResult . Users . Select ( userRecord => userRecord . Uid ) ;
839+ var expectedUids = new List < string > ( ) { this . testUser1 . Uid , this . testUser2 . Uid , this . testUser3 . Uid } ;
840+ Assert . True ( expectedUids . All ( expectedUid => uids . Contains ( expectedUid ) ) ) ;
841+ Assert . Empty ( getUsersResult . NotFound ) ;
842+ }
843+
844+ [ Fact ]
845+ public async void IgnoresNonExistingUsers ( )
846+ {
847+ var doesntExistId = new UidIdentifier ( "uid_that_doesnt_exist" ) ;
848+ var getUsersResult = await this . auth . GetUsersAsync ( new List < UserIdentifier > ( )
849+ {
850+ new UidIdentifier ( this . testUser1 . Uid ) ,
851+ doesntExistId ,
852+ new UidIdentifier ( this . testUser3 . Uid ) ,
853+ } ) ;
854+
855+ var uids = getUsersResult . Users . Select ( userRecord => userRecord . Uid ) ;
856+ var expectedUids = new List < string > ( ) { this . testUser1 . Uid , this . testUser3 . Uid } ;
857+ Assert . True ( expectedUids . All ( expectedUid => uids . Contains ( expectedUid ) ) ) ;
858+ Assert . Equal ( doesntExistId , getUsersResult . NotFound . Single ( ) ) ;
859+ }
860+
861+ [ Fact ]
862+ public async void OnlyNonExistingUsers ( )
863+ {
864+ var doesntExistId = new UidIdentifier ( "uid_that_doesnt_exist" ) ;
865+ var getUsersResult = await this . auth . GetUsersAsync ( new List < UserIdentifier > ( )
866+ {
867+ doesntExistId ,
868+ } ) ;
869+
870+ Assert . Empty ( getUsersResult . Users ) ;
871+ Assert . Equal ( doesntExistId , getUsersResult . NotFound . Single ( ) ) ;
872+ }
873+
874+ [ Fact ]
875+ public async void DedupsDuplicateUsers ( )
876+ {
877+ var getUsersResult = await this . auth . GetUsersAsync ( new List < UserIdentifier > ( )
878+ {
879+ new UidIdentifier ( this . testUser1 . Uid ) ,
880+ new UidIdentifier ( this . testUser1 . Uid ) ,
881+ } ) ;
882+
883+ var uids = getUsersResult . Users . Select ( userRecord => userRecord . Uid ) ;
884+ var expectedUids = new List < string > ( ) { this . testUser3 . Uid } ;
885+ Assert . Equal ( this . testUser1 . Uid , getUsersResult . Users . Single ( ) . Uid ) ;
886+ Assert . Empty ( getUsersResult . NotFound ) ;
887+ }
888+ }
613889 }
614890
615891 /**
@@ -656,6 +932,24 @@ internal class SignInResponse
656932 public string IdToken { get ; set ; }
657933 }
658934
935+ internal class VerifyPasswordRequest
936+ {
937+ [ Newtonsoft . Json . JsonProperty ( "email" ) ]
938+ public string Email { get ; set ; }
939+
940+ [ Newtonsoft . Json . JsonProperty ( "password" ) ]
941+ public string Password { get ; set ; }
942+
943+ [ Newtonsoft . Json . JsonProperty ( "returnSecureToken" ) ]
944+ public bool ReturnSecureToken { get ; set ; }
945+ }
946+
947+ internal class VerifyPasswordResponse
948+ {
949+ [ Newtonsoft . Json . JsonProperty ( "idToken" ) ]
950+ public string IdToken { get ; set ; }
951+ }
952+
659953 internal class RandomUser
660954 {
661955 internal string Uid { get ; private set ; }
0 commit comments