Skip to content

Commit d51d33c

Browse files
authored
feat(auth): Implemented GeneratePasswordResetLinkAsync() API (#162)
* Added GeneratePasswordResetLinkAsync() API * Moved tests to a new class * Cleaned up the action type enum * Restricting CI triggers to pull_request events * Some doc updates
1 parent 7c972d8 commit d51d33c

File tree

8 files changed

+504
-7
lines changed

8 files changed

+504
-7
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: Continuous Integration
22

3-
on: push
3+
on: pull_request
44

55
jobs:
66
build:
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
// Copyright 2020, Google Inc. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
using System.Collections.Generic;
17+
using System.Linq;
18+
using System.Net.Http;
19+
using System.Threading.Tasks;
20+
using FirebaseAdmin.Tests;
21+
using FirebaseAdmin.Util;
22+
using Google.Apis.Auth.OAuth2;
23+
using Google.Apis.Json;
24+
using Xunit;
25+
26+
namespace FirebaseAdmin.Auth.Tests
27+
{
28+
public class EmailActionRequestTest
29+
{
30+
public static readonly IEnumerable<object[]> InvalidActionCodeSettingsArgs =
31+
new List<object[]>()
32+
{
33+
new object[] { new ActionCodeSettings() },
34+
new object[] { new ActionCodeSettings() { Url = string.Empty } },
35+
new object[] { new ActionCodeSettings() { Url = "not a url" } },
36+
new object[]
37+
{
38+
new ActionCodeSettings()
39+
{
40+
Url = "https://example.dynamic.link",
41+
AndroidInstallApp = true,
42+
},
43+
},
44+
new object[]
45+
{
46+
new ActionCodeSettings()
47+
{
48+
Url = "https://example.dynamic.link",
49+
DynamicLinkDomain = string.Empty,
50+
},
51+
},
52+
new object[]
53+
{
54+
new ActionCodeSettings()
55+
{
56+
Url = "https://example.dynamic.link",
57+
AndroidMinimumVersion = string.Empty,
58+
},
59+
},
60+
new object[]
61+
{
62+
new ActionCodeSettings()
63+
{
64+
Url = "https://example.dynamic.link",
65+
AndroidPackageName = string.Empty,
66+
},
67+
},
68+
new object[]
69+
{
70+
new ActionCodeSettings()
71+
{
72+
Url = "https://example.dynamic.link",
73+
IosBundleId = string.Empty,
74+
},
75+
},
76+
};
77+
78+
private const string GenerateEmailLinkResponse = @"{
79+
""oobLink"": ""https://mock-oob-link.for.auth.tests""
80+
}";
81+
82+
private static readonly ActionCodeSettings ActionCodeSettings = new ActionCodeSettings()
83+
{
84+
Url = "https://example.dynamic.link",
85+
HandleCodeInApp = true,
86+
DynamicLinkDomain = "custom.page.link",
87+
IosBundleId = "com.example.ios",
88+
AndroidPackageName = "com.example.android",
89+
AndroidMinimumVersion = "6",
90+
AndroidInstallApp = true,
91+
};
92+
93+
[Fact]
94+
public void NoEmail()
95+
{
96+
var handler = new MockMessageHandler() { Response = GenerateEmailLinkResponse };
97+
var auth = this.CreateFirebaseAuth(handler);
98+
99+
Assert.ThrowsAsync<ArgumentException>(
100+
async () => await auth.GeneratePasswordResetLinkAsync(null));
101+
Assert.ThrowsAsync<ArgumentException>(
102+
async () => await auth.GeneratePasswordResetLinkAsync(string.Empty));
103+
}
104+
105+
[Theory]
106+
[MemberData(nameof(InvalidActionCodeSettingsArgs))]
107+
public void InvalidActionCodeSettings(ActionCodeSettings settings)
108+
{
109+
var handler = new MockMessageHandler() { Response = GenerateEmailLinkResponse };
110+
var auth = this.CreateFirebaseAuth(handler);
111+
var email = "user@example.com";
112+
113+
Assert.ThrowsAsync<ArgumentException>(
114+
async () => await auth.GeneratePasswordResetLinkAsync(email, settings));
115+
}
116+
117+
[Fact]
118+
public async Task PasswordResetLink()
119+
{
120+
var handler = new MockMessageHandler() { Response = GenerateEmailLinkResponse };
121+
var auth = this.CreateFirebaseAuth(handler);
122+
123+
var link = await auth.GeneratePasswordResetLinkAsync("user@example.com");
124+
125+
Assert.Equal("https://mock-oob-link.for.auth.tests", link);
126+
127+
var request = NewtonsoftJsonSerializer.Instance.Deserialize<Dictionary<string, object>>(
128+
handler.LastRequestBody);
129+
Assert.Equal(3, request.Count);
130+
Assert.Equal("user@example.com", request["email"]);
131+
Assert.Equal("PASSWORD_RESET", request["requestType"]);
132+
Assert.True((bool)request["returnOobLink"]);
133+
this.AssertRequest(handler.Requests[0]);
134+
}
135+
136+
[Fact]
137+
public async Task PasswordResetLinkWithSettings()
138+
{
139+
var handler = new MockMessageHandler() { Response = GenerateEmailLinkResponse };
140+
var auth = this.CreateFirebaseAuth(handler);
141+
142+
var link = await auth.GeneratePasswordResetLinkAsync(
143+
"user@example.com", ActionCodeSettings);
144+
145+
Assert.Equal("https://mock-oob-link.for.auth.tests", link);
146+
147+
var request = NewtonsoftJsonSerializer.Instance.Deserialize<Dictionary<string, object>>(
148+
handler.LastRequestBody);
149+
Assert.Equal(10, request.Count);
150+
Assert.Equal("user@example.com", request["email"]);
151+
Assert.Equal("PASSWORD_RESET", request["requestType"]);
152+
Assert.True((bool)request["returnOobLink"]);
153+
154+
Assert.Equal(ActionCodeSettings.Url, request["continueUrl"]);
155+
Assert.True((bool)request["canHandleCodeInApp"]);
156+
Assert.Equal(ActionCodeSettings.DynamicLinkDomain, request["dynamicLinkDomain"]);
157+
Assert.Equal(ActionCodeSettings.IosBundleId, request["iOSBundleId"]);
158+
Assert.Equal(ActionCodeSettings.AndroidPackageName, request["androidPackageName"]);
159+
Assert.Equal(
160+
ActionCodeSettings.AndroidMinimumVersion, request["androidMinimumVersion"]);
161+
Assert.True((bool)request["androidInstallApp"]);
162+
this.AssertRequest(handler.Requests[0]);
163+
}
164+
165+
[Fact]
166+
public async Task PasswordResetLinkUnexpectedResponse()
167+
{
168+
var handler = new MockMessageHandler() { Response = "{}" };
169+
var auth = this.CreateFirebaseAuth(handler);
170+
171+
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
172+
async () => await auth.GeneratePasswordResetLinkAsync("user@example.com"));
173+
174+
Assert.Equal(ErrorCode.Unknown, exception.ErrorCode);
175+
Assert.Equal(AuthErrorCode.UnexpectedResponse, exception.AuthErrorCode);
176+
Assert.Equal(
177+
$"Failed to generate email action link for: user@example.com",
178+
exception.Message);
179+
Assert.NotNull(exception.HttpResponse);
180+
Assert.Null(exception.InnerException);
181+
}
182+
183+
private FirebaseAuth CreateFirebaseAuth(HttpMessageHandler handler)
184+
{
185+
var userManager = new FirebaseUserManager(new FirebaseUserManager.Args
186+
{
187+
Credential = GoogleCredential.FromAccessToken("test-token"),
188+
ProjectId = "project1",
189+
ClientFactory = new MockHttpClientFactory(handler),
190+
RetryOptions = RetryOptions.NoBackOff,
191+
});
192+
return new FirebaseAuth(new FirebaseAuth.FirebaseAuthArgs()
193+
{
194+
UserManager = new Lazy<FirebaseUserManager>(userManager),
195+
TokenFactory = new Lazy<FirebaseTokenFactory>(),
196+
IdTokenVerifier = new Lazy<FirebaseTokenVerifier>(),
197+
});
198+
}
199+
200+
private void AssertRequest(MockMessageHandler.IncomingRequest message)
201+
{
202+
Assert.Equal(message.Method, HttpMethod.Post);
203+
Assert.EndsWith("/accounts:sendOobCode", message.Url.PathAndQuery);
204+
Assert.Equal(
205+
FirebaseUserManager.ClientVersion,
206+
message.Headers.GetValues(FirebaseUserManager.ClientVersionHeader).First());
207+
}
208+
}
209+
}

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ namespace FirebaseAdmin.Auth.Tests
3131
public class FirebaseUserManagerTest
3232
{
3333
private const string MockProjectId = "project1";
34+
private const string CreateUserResponse = @"{""localId"": ""user1""}";
35+
private const string GetUserResponse = @"{""users"": [{""localId"": ""user1""}]}";
3436

3537
private static readonly GoogleCredential MockCredential =
3638
GoogleCredential.FromAccessToken("test-token");
3739

38-
private static readonly string CreateUserResponse = @"{""localId"": ""user1""}";
39-
private static readonly string GetUserResponse = @"{""users"": [{""localId"": ""user1""}]}";
4040
private static readonly IList<string> ListUsersResponse = new List<string>()
4141
{
4242
@"{
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2020, Google Inc. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
namespace FirebaseAdmin.Auth
16+
{
17+
/// <summary>
18+
/// Defines the required continue/state URL with optional Android and iOS settings. Used when
19+
/// invoking the email action link generation APIs in <see cref="FirebaseAuth"/>.
20+
/// </summary>
21+
public sealed class ActionCodeSettings
22+
{
23+
/// <summary>
24+
/// Gets or sets the continue/state URL. This property has different meanings in different
25+
/// contexts.
26+
/// <list type="bullet">
27+
/// <item>
28+
/// <description>
29+
/// When the link is handled in the web action widgets, this is the deep link in the
30+
/// <c>continueUrl</c> query parameter.
31+
/// </description>
32+
/// </item>
33+
/// <item>
34+
/// <description>
35+
/// When the link is handled in the app directly, this is the <c>continueUrl</c> query
36+
/// parameter in the deep link of the Dynamic Link.
37+
/// </description>
38+
/// </item>
39+
/// </list>
40+
/// This property is required.
41+
/// </summary>
42+
public string Url { get; set; }
43+
44+
/// <summary>
45+
/// Gets or sets a value indicating whether to open the link via a mobile app or a browser.
46+
/// The default is false. When set to true, the action code link is sent as a Universal
47+
/// Link or an Android App Link and is opened by the app if installed. In the false case,
48+
/// the code is sent to the web widget first and then redirects to the app if installed.
49+
/// </summary>
50+
public bool HandleCodeInApp { get; set; }
51+
52+
/// <summary>
53+
/// Gets or sets the dynamic link domain to use for the current link if it is to be opened
54+
/// using Firebase Dynamic Links, as multiple dynamic link domains can be configured per
55+
/// project. This setting provides the ability to explicitly choose one. If none is provided,
56+
/// the oldest domain is used by default.
57+
/// </summary>
58+
public string DynamicLinkDomain { get; set; }
59+
60+
/// <summary>
61+
/// Gets or sets the bundle ID of the iOS app where the link should be handled if the
62+
/// application is already installed on the device.
63+
/// </summary>
64+
public string IosBundleId { get; set; }
65+
66+
/// <summary>
67+
/// Gets or sets the Android package name of the app where the link should be handled if
68+
/// the Android app is installed. Must be specified when setting other Android-specific
69+
/// settings.
70+
/// </summary>
71+
public string AndroidPackageName { get; set; }
72+
73+
/// <summary>
74+
/// Gets or sets the minimum version for the Android app. If the installed app is an older
75+
/// version, the user is taken to the Play Store to upgrade the app.
76+
/// </summary>
77+
public string AndroidMinimumVersion { get; set; }
78+
79+
/// <summary>
80+
/// Gets or sets a value indicating whether to install the Android app if the device
81+
/// supports it and the app is not already installed.
82+
/// </summary>
83+
public bool AndroidInstallApp { get; set; }
84+
}
85+
}

0 commit comments

Comments
 (0)