Skip to content

Commit 8a3f048

Browse files
fix: handle OAuth token validation and error handling (#462)
Changes: - OAuthAccessToken: Added TokenType field and improved validation logic. - OAuthSdkCredentials: Fixed grant_type position in request payload. - OAuthServiceResponse: Enhanced IsValid method with stricter checks. - Tests: - Added tests for OAuthAccessToken validation. - Added tests for OAuthSdkCredentials error handling. - Added tests for OAuthServiceResponse validation. - Mocked HTTP responses for OAuth SDK credentials.
1 parent 365c237 commit 8a3f048

File tree

6 files changed

+203
-12
lines changed

6 files changed

+203
-12
lines changed

src/CheckoutSdk/OAuthAccessToken.cs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,29 @@ namespace Checkout
55
public sealed class OAuthAccessToken
66
{
77
public string Token { get; }
8+
public string TokenType { get; }
89
private readonly DateTime? _expirationDate;
910

1011
public static OAuthAccessToken FromOAuthServiceResponse(OAuthServiceResponse response)
1112
{
12-
return new OAuthAccessToken(response.AccessToken,
13-
DateTime.Now.Add(TimeSpan.FromSeconds(response.ExpiresIn)));
13+
if (!response.IsValid())
14+
{
15+
throw new ArgumentException("Invalid OAuth response");
16+
}
17+
18+
return new OAuthAccessToken(
19+
response.AccessToken,
20+
response.TokenType,
21+
DateTime.UtcNow.Add(TimeSpan.FromSeconds(response.ExpiresIn)));
1422
}
1523

16-
private OAuthAccessToken(string token, DateTime expirationDate)
24+
private OAuthAccessToken(string token, string tokenType, DateTime expirationDate)
1725
{
18-
Token = token;
19-
_expirationDate = expirationDate;
26+
Token = !string.IsNullOrWhiteSpace(token) ? token : throw new ArgumentException("Token cannot be empty");
27+
TokenType = !string.IsNullOrWhiteSpace(tokenType)
28+
? tokenType
29+
: throw new ArgumentException("TokenType cannot be empty");
30+
_expirationDate = expirationDate.ToUniversalTime();
2031
}
2132

2233
public bool IsValid()
@@ -26,7 +37,7 @@ public bool IsValid()
2637
return false;
2738
}
2839

29-
return _expirationDate > DateTime.Now;
40+
return _expirationDate > DateTime.UtcNow;
3041
}
3142
}
3243
}

src/CheckoutSdk/OAuthSdkCredentials.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ public sealed class OAuthSdkCredentials : SdkCredentials
1313
#if (NETSTANDARD2_0_OR_GREATER || NETCOREAPP3_1_OR_GREATER)
1414
private readonly ILogger _log = LogProvider.GetLogger(typeof(OAuthSdkCredentials));
1515
#endif
16-
1716
private readonly string _clientId;
1817
private readonly string _clientSecret;
1918
private readonly JsonSerializer _serializer = new JsonSerializer();
@@ -80,9 +79,9 @@ private OAuthServiceResponse Request()
8079
var httpRequest = new HttpRequestMessage(HttpMethod.Post, string.Empty);
8180
var data = new List<KeyValuePair<string, string>>
8281
{
82+
new KeyValuePair<string, string>("grant_type", "client_credentials"),
8383
new KeyValuePair<string, string>("client_id", _clientId),
8484
new KeyValuePair<string, string>("client_secret", _clientSecret),
85-
new KeyValuePair<string, string>("grant_type", "client_credentials"),
8685
new KeyValuePair<string, string>("scope", GetScopes())
8786
};
8887
httpRequest.Content = new FormUrlEncodedContent(data);

src/CheckoutSdk/OAuthServiceResponse.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ public sealed class OAuthServiceResponse
44
{
55
public string AccessToken { get; set; }
66

7+
public string TokenType { get; set; }
8+
79
public long ExpiresIn { get; set; }
810

911
public string Error { get; set; }
1012

11-
public bool IsValid()
12-
{
13-
return AccessToken != null && ExpiresIn != 0 && Error == null;
14-
}
13+
public bool IsValid() =>
14+
!string.IsNullOrWhiteSpace(AccessToken) &&
15+
!string.IsNullOrWhiteSpace(TokenType) &&
16+
ExpiresIn > 0 &&
17+
string.IsNullOrWhiteSpace(Error);
1518
}
1619
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System;
2+
using Xunit;
3+
4+
namespace Checkout
5+
{
6+
public class OAuthAccessTokenTests
7+
{
8+
[Fact]
9+
public void ShouldReturnTrueAndTokenWhenResponseIsValid()
10+
{
11+
var response = new OAuthServiceResponse
12+
{
13+
AccessToken = "valid_token",
14+
TokenType = "Bearer",
15+
ExpiresIn = 3600
16+
};
17+
18+
var token = OAuthAccessToken.FromOAuthServiceResponse(response);
19+
20+
Assert.NotNull(token);
21+
Assert.Equal("valid_token", token.Token);
22+
Assert.True(token.IsValid());
23+
}
24+
25+
[Fact]
26+
public void ShouldThrowExceptionWhenResponseIsInvalid()
27+
{
28+
var response = new OAuthServiceResponse
29+
{
30+
AccessToken = null,
31+
TokenType = "Bearer",
32+
ExpiresIn = 3600
33+
};
34+
35+
Assert.Throws<ArgumentException>(() => OAuthAccessToken.FromOAuthServiceResponse(response));
36+
}
37+
}
38+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using Moq;
2+
using Moq.Protected;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Net;
6+
using System.Net.Http;
7+
using System.Text;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Xunit;
11+
12+
namespace Checkout
13+
{
14+
public class OAuthSdkCredentialsTests
15+
{
16+
private OAuthSdkCredentials CreateSdkCredentials(HttpResponseMessage mockResponse)
17+
{
18+
var mockHttpMessageHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
19+
20+
mockHttpMessageHandler
21+
.Protected()
22+
.Setup<Task<HttpResponseMessage>>(
23+
"SendAsync",
24+
ItExpr.IsAny<HttpRequestMessage>(),
25+
ItExpr.IsAny<CancellationToken>())
26+
.ReturnsAsync(mockResponse)
27+
.Verifiable();
28+
29+
var httpClient = new HttpClient(mockHttpMessageHandler.Object)
30+
{
31+
BaseAddress = new Uri("https://fake-auth.com")
32+
};
33+
34+
var httpClientFactoryMock = new Mock<IHttpClientFactory>();
35+
httpClientFactoryMock
36+
.Setup(_ => _.CreateClient())
37+
.Returns(httpClient);
38+
39+
return new OAuthSdkCredentials(
40+
httpClientFactoryMock.Object,
41+
new Uri("https://fake-auth.com"),
42+
"test_client_id",
43+
"test_client_secret",
44+
new HashSet<OAuthScope>()
45+
);
46+
}
47+
48+
[Fact]
49+
public void ShouldReturnAuthorizationHeaderWhenTokenIsValid()
50+
{
51+
using var mockResponse = new HttpResponseMessage();
52+
mockResponse.StatusCode = HttpStatusCode.OK;
53+
mockResponse.Content = new StringContent(
54+
"{\"access_token\": \"valid_token\", \"token_type\": \"Bearer\", \"expires_in\": 3600}",
55+
Encoding.UTF8,
56+
"application/json");
57+
var sdk = CreateSdkCredentials(mockResponse);
58+
sdk.InitAccess();
59+
60+
var authorization = sdk.GetSdkAuthorization(SdkAuthorizationType.OAuth);
61+
Assert.NotNull(authorization);
62+
63+
string expectedHeader = $"Bearer valid_token";
64+
string actualHeader = authorization.GetAuthorizationHeader();
65+
66+
Assert.Equal(expectedHeader, actualHeader);
67+
}
68+
69+
[Fact]
70+
public void ShouldThrowExceptionWhenApiReturnsError()
71+
{
72+
using var mockResponse = new HttpResponseMessage();
73+
mockResponse.StatusCode = HttpStatusCode.BadRequest;
74+
mockResponse.Content = new StringContent(
75+
"{\"error\": \"invalid_client\"}",
76+
Encoding.UTF8,
77+
"application/json");
78+
var sdk = CreateSdkCredentials(mockResponse);
79+
80+
Assert.Throws<CheckoutAuthorizationException>(() => sdk.InitAccess());
81+
}
82+
83+
[Fact]
84+
public void ShouldThrowExceptionWhenResponseHasInvalidToken()
85+
{
86+
var response = new OAuthServiceResponse { AccessToken = null, TokenType = "Bearer", ExpiresIn = 3600 };
87+
88+
Assert.Throws<ArgumentException>(() => OAuthAccessToken.FromOAuthServiceResponse(response));
89+
}
90+
}
91+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using Xunit;
2+
3+
namespace Checkout
4+
{
5+
public class OAuthServiceResponseTests
6+
{
7+
[Fact]
8+
public void ShouldReturnTrueWhenResponseIsValid()
9+
{
10+
var response = new OAuthServiceResponse
11+
{
12+
AccessToken = "valid_token",
13+
TokenType = "Bearer",
14+
ExpiresIn = 3600,
15+
Error = null
16+
};
17+
18+
Assert.True(response.IsValid());
19+
}
20+
21+
[Fact]
22+
public void ShouldReturnFalseWhenResponseIsInvalid()
23+
{
24+
var response = new OAuthServiceResponse
25+
{
26+
AccessToken = null,
27+
TokenType = "Bearer",
28+
ExpiresIn = 3600,
29+
Error = null
30+
};
31+
32+
Assert.False(response.IsValid());
33+
}
34+
35+
[Fact]
36+
public void ShouldReturnFalseWhenExpiresInIsNegative()
37+
{
38+
var response = new OAuthServiceResponse
39+
{
40+
AccessToken = "valid_token",
41+
TokenType = "Bearer",
42+
ExpiresIn = -1,
43+
Error = null
44+
};
45+
46+
Assert.False(response.IsValid());
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)