Skip to content

Commit f0f7507

Browse files
committed
Implemented error handler for Auth API calls
1 parent 1cd6e4a commit f0f7507

File tree

6 files changed

+423
-3
lines changed

6 files changed

+423
-3
lines changed
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// Copyright 2019, 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.Collections.Generic;
16+
using System.Net;
17+
using System.Net.Http;
18+
using System.Text;
19+
using Xunit;
20+
21+
namespace FirebaseAdmin.Auth.Tests
22+
{
23+
public class AuthErrorHandlerTest
24+
{
25+
public static readonly IEnumerable<object[]> AuthErrorCodes =
26+
new List<object[]>()
27+
{
28+
new object[]
29+
{
30+
"DUPLICATE_LOCAL_ID",
31+
ErrorCode.AlreadyExists,
32+
AuthErrorCode.UidAlreadyExists,
33+
},
34+
};
35+
36+
[Theory]
37+
[MemberData(nameof(AuthErrorCodes))]
38+
public void KnownErrorCode(
39+
string code, ErrorCode expectedCode, AuthErrorCode expectedAuthCode)
40+
{
41+
var json = $@"{{
42+
""error"": {{
43+
""message"": ""{code}"",
44+
}}
45+
}}";
46+
var resp = new HttpResponseMessage()
47+
{
48+
StatusCode = HttpStatusCode.ServiceUnavailable,
49+
Content = new StringContent(json, Encoding.UTF8, "application/json"),
50+
};
51+
52+
var handler = new AuthErrorHandler();
53+
var error = Assert.Throws<FirebaseAuthException>(() => handler.ThrowIfError(resp, json));
54+
55+
Assert.Equal(expectedCode, error.ErrorCode);
56+
Assert.Equal(expectedAuthCode, error.AuthErrorCode);
57+
Assert.Same(resp, error.HttpResponse);
58+
Assert.Null(error.InnerException);
59+
Assert.EndsWith($" ({code}).", error.Message);
60+
}
61+
62+
[Theory]
63+
[MemberData(nameof(AuthErrorCodes))]
64+
public void KnownErrorCodeWithDetails(
65+
string code, ErrorCode expectedCode, AuthErrorCode expectedAuthCode)
66+
{
67+
var json = $@"{{
68+
""error"": {{
69+
""message"": ""{code}: Some details."",
70+
}}
71+
}}";
72+
var resp = new HttpResponseMessage()
73+
{
74+
StatusCode = HttpStatusCode.ServiceUnavailable,
75+
Content = new StringContent(json, Encoding.UTF8, "application/json"),
76+
};
77+
78+
var handler = new AuthErrorHandler();
79+
var error = Assert.Throws<FirebaseAuthException>(() => handler.ThrowIfError(resp, json));
80+
81+
Assert.Equal(expectedCode, error.ErrorCode);
82+
Assert.Equal(expectedAuthCode, error.AuthErrorCode);
83+
Assert.Same(resp, error.HttpResponse);
84+
Assert.Null(error.InnerException);
85+
Assert.EndsWith($" ({code}): Some details.", error.Message);
86+
}
87+
88+
[Fact]
89+
public void UnknownErrorCode()
90+
{
91+
var json = $@"{{
92+
""error"": {{
93+
""message"": ""SOMETHING_UNUSUAL"",
94+
}}
95+
}}";
96+
var resp = new HttpResponseMessage()
97+
{
98+
StatusCode = HttpStatusCode.InternalServerError,
99+
Content = new StringContent(json, Encoding.UTF8, "application/json"),
100+
};
101+
102+
var handler = new AuthErrorHandler();
103+
var error = Assert.Throws<FirebaseAuthException>(() => handler.ThrowIfError(resp, json));
104+
105+
Assert.Equal(ErrorCode.Internal, error.ErrorCode);
106+
Assert.Equal(
107+
$"Unexpected HTTP response with status: 500 (InternalServerError)\n{json}",
108+
error.Message);
109+
Assert.Null(error.AuthErrorCode);
110+
Assert.Same(resp, error.HttpResponse);
111+
Assert.Null(error.InnerException);
112+
}
113+
114+
[Fact]
115+
public void UnspecifiedErrorCode()
116+
{
117+
var json = $@"{{
118+
""error"": {{}}
119+
}}";
120+
var resp = new HttpResponseMessage()
121+
{
122+
StatusCode = HttpStatusCode.InternalServerError,
123+
Content = new StringContent(json, Encoding.UTF8, "application/json"),
124+
};
125+
126+
var handler = new AuthErrorHandler();
127+
var error = Assert.Throws<FirebaseAuthException>(() => handler.ThrowIfError(resp, json));
128+
129+
Assert.Equal(ErrorCode.Internal, error.ErrorCode);
130+
Assert.Equal(
131+
$"Unexpected HTTP response with status: 500 (InternalServerError)\n{json}",
132+
error.Message);
133+
Assert.Null(error.AuthErrorCode);
134+
Assert.Same(resp, error.HttpResponse);
135+
Assert.Null(error.InnerException);
136+
}
137+
138+
[Fact]
139+
public void NoDetails()
140+
{
141+
var json = @"{}";
142+
var resp = new HttpResponseMessage()
143+
{
144+
StatusCode = HttpStatusCode.ServiceUnavailable,
145+
Content = new StringContent(json, Encoding.UTF8, "application/json"),
146+
};
147+
148+
var handler = new AuthErrorHandler();
149+
var error = Assert.Throws<FirebaseAuthException>(() => handler.ThrowIfError(resp, json));
150+
151+
Assert.Equal(ErrorCode.Unavailable, error.ErrorCode);
152+
Assert.Equal(
153+
"Unexpected HTTP response with status: 503 (ServiceUnavailable)\n{}",
154+
error.Message);
155+
Assert.Null(error.AuthErrorCode);
156+
Assert.Same(resp, error.HttpResponse);
157+
Assert.Null(error.InnerException);
158+
}
159+
160+
[Fact]
161+
public void NonJson()
162+
{
163+
var text = "plain text";
164+
var resp = new HttpResponseMessage()
165+
{
166+
StatusCode = HttpStatusCode.ServiceUnavailable,
167+
Content = new StringContent(text, Encoding.UTF8, "text/plain"),
168+
};
169+
170+
var handler = new AuthErrorHandler();
171+
var error = Assert.Throws<FirebaseAuthException>(() => handler.ThrowIfError(resp, text));
172+
173+
Assert.Equal(ErrorCode.Unavailable, error.ErrorCode);
174+
Assert.Equal(
175+
$"Unexpected HTTP response with status: 503 (ServiceUnavailable)\n{text}",
176+
error.Message);
177+
Assert.Null(error.AuthErrorCode);
178+
Assert.Same(resp, error.HttpResponse);
179+
Assert.Null(error.InnerException);
180+
}
181+
}
182+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2019, 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+
/// Error codes that can be raised by the Firebase Auth APIs.
19+
/// </summary>
20+
public enum AuthErrorCode
21+
{
22+
/// <summary>
23+
/// The user with the provided uid already exists.
24+
/// </summary>
25+
UidAlreadyExists,
26+
}
27+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Copyright 2019, 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.Collections.Generic;
16+
using System.Net.Http;
17+
using Google.Apis.Json;
18+
using Newtonsoft.Json;
19+
20+
namespace FirebaseAdmin.Auth
21+
{
22+
/// <summary>
23+
/// Parses error responses received from the Auth service, and creates instances of
24+
/// <see cref="FirebaseAuthException"/>.
25+
/// </summary>
26+
internal sealed class AuthErrorHandler : HttpErrorHandler
27+
{
28+
private static readonly IReadOnlyDictionary<string, ErrorInfo> CodeToErrorInfo =
29+
new Dictionary<string, ErrorInfo>()
30+
{
31+
{
32+
"DUPLICATE_LOCAL_ID",
33+
new ErrorInfo(
34+
ErrorCode.AlreadyExists,
35+
AuthErrorCode.UidAlreadyExists,
36+
"The user with the provided uid already exists")
37+
},
38+
};
39+
40+
protected sealed override FirebaseExceptionArgs CreateExceptionArgs(
41+
HttpResponseMessage response, string body)
42+
{
43+
var authError = this.ParseAuthError(body);
44+
45+
ErrorInfo info;
46+
CodeToErrorInfo.TryGetValue(authError.Code, out info);
47+
48+
var defaults = base.CreateExceptionArgs(response, body);
49+
return new FirebaseAuthExceptionArgs()
50+
{
51+
Code = info?.ErrorCode ?? defaults.Code,
52+
Message = info?.GetMessage(authError) ?? defaults.Message,
53+
HttpResponse = response,
54+
ResponseBody = body,
55+
AuthErrorCode = info?.AuthErrorCode,
56+
};
57+
}
58+
59+
protected override FirebaseException CreateException(FirebaseExceptionArgs args)
60+
{
61+
return new FirebaseAuthException(
62+
args.Code,
63+
args.Message,
64+
(args as FirebaseAuthExceptionArgs).AuthErrorCode,
65+
response: args.HttpResponse);
66+
}
67+
68+
private AuthError ParseAuthError(string body)
69+
{
70+
try
71+
{
72+
var parsed = NewtonsoftJsonSerializer.Instance.Deserialize<AuthErrorResponse>(body);
73+
return parsed.Error ?? new AuthError();
74+
}
75+
catch
76+
{
77+
// Ignore any error that may occur while parsing the error response. The server
78+
// may have responded with a non-json body.
79+
return new AuthError();
80+
}
81+
}
82+
83+
/// <summary>
84+
/// Describes a class of errors that can be raised by the Firebase Auth backend API.
85+
/// </summary>
86+
private sealed class ErrorInfo
87+
{
88+
private readonly string message;
89+
90+
internal ErrorInfo(ErrorCode code, AuthErrorCode authCode, string message)
91+
{
92+
this.ErrorCode = code;
93+
this.AuthErrorCode = authCode;
94+
this.message = message;
95+
}
96+
97+
internal ErrorCode ErrorCode { get; private set; }
98+
99+
internal AuthErrorCode AuthErrorCode { get; private set; }
100+
101+
internal string GetMessage(AuthError authError)
102+
{
103+
var message = $"{this.message} ({authError.Code})";
104+
if (!string.IsNullOrEmpty(authError.Detail))
105+
{
106+
return $"{message}: {authError.Detail}";
107+
}
108+
109+
return $"{message}.";
110+
}
111+
}
112+
113+
private sealed class FirebaseAuthExceptionArgs : FirebaseExceptionArgs
114+
{
115+
internal AuthErrorCode? AuthErrorCode { get; set; }
116+
}
117+
118+
private sealed class AuthError
119+
{
120+
[JsonProperty("message")]
121+
internal string Message { get; set; }
122+
123+
/// <summary>
124+
/// Gets the Firebase Auth error code extracted from the response. Returns empty string
125+
/// if the error code cannot be determined.
126+
/// </summary>
127+
internal string Code
128+
{
129+
get
130+
{
131+
var separator = this.GetSeparator();
132+
if (separator != -1)
133+
{
134+
return this.Message.Substring(0, separator);
135+
}
136+
137+
return this.Message ?? string.Empty;
138+
}
139+
}
140+
141+
/// <summary>
142+
/// Gets the error detail sent by the Firebase Auth API. May be null.
143+
/// </summary>
144+
internal string Detail
145+
{
146+
get
147+
{
148+
var separator = this.GetSeparator();
149+
if (separator != -1)
150+
{
151+
return this.Message.Substring(separator + 1).Trim();
152+
}
153+
154+
return null;
155+
}
156+
}
157+
158+
private int GetSeparator()
159+
{
160+
return this.Message?.IndexOf(':') ?? -1;
161+
}
162+
}
163+
164+
private sealed class AuthErrorResponse
165+
{
166+
[JsonProperty("error")]
167+
internal AuthError Error { get; set; }
168+
}
169+
}
170+
}

0 commit comments

Comments
 (0)