From cedded8d983816193010cd444df09688cc60648d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= Date: Mon, 17 Mar 2025 18:45:18 +0100 Subject: [PATCH] fix: handle OAuth token validation and error handling 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. --- src/CheckoutSdk/OAuthAccessToken.cs | 23 +++-- src/CheckoutSdk/OAuthSdkCredentials.cs | 3 +- src/CheckoutSdk/OAuthServiceResponse.cs | 11 ++- test/CheckoutSdkTest/OAuthAccessTokenTests.cs | 38 ++++++++ .../OAuthSdkCredentialsTests.cs | 91 +++++++++++++++++++ .../OAuthServiceResponseTests.cs | 49 ++++++++++ 6 files changed, 203 insertions(+), 12 deletions(-) create mode 100644 test/CheckoutSdkTest/OAuthAccessTokenTests.cs create mode 100644 test/CheckoutSdkTest/OAuthSdkCredentialsTests.cs create mode 100644 test/CheckoutSdkTest/OAuthServiceResponseTests.cs diff --git a/src/CheckoutSdk/OAuthAccessToken.cs b/src/CheckoutSdk/OAuthAccessToken.cs index 52c060ff..756b5003 100644 --- a/src/CheckoutSdk/OAuthAccessToken.cs +++ b/src/CheckoutSdk/OAuthAccessToken.cs @@ -5,18 +5,29 @@ namespace Checkout public sealed class OAuthAccessToken { public string Token { get; } + public string TokenType { get; } private readonly DateTime? _expirationDate; public static OAuthAccessToken FromOAuthServiceResponse(OAuthServiceResponse response) { - return new OAuthAccessToken(response.AccessToken, - DateTime.Now.Add(TimeSpan.FromSeconds(response.ExpiresIn))); + if (!response.IsValid()) + { + throw new ArgumentException("Invalid OAuth response"); + } + + return new OAuthAccessToken( + response.AccessToken, + response.TokenType, + DateTime.UtcNow.Add(TimeSpan.FromSeconds(response.ExpiresIn))); } - private OAuthAccessToken(string token, DateTime expirationDate) + private OAuthAccessToken(string token, string tokenType, DateTime expirationDate) { - Token = token; - _expirationDate = expirationDate; + Token = !string.IsNullOrWhiteSpace(token) ? token : throw new ArgumentException("Token cannot be empty"); + TokenType = !string.IsNullOrWhiteSpace(tokenType) + ? tokenType + : throw new ArgumentException("TokenType cannot be empty"); + _expirationDate = expirationDate.ToUniversalTime(); } public bool IsValid() @@ -26,7 +37,7 @@ public bool IsValid() return false; } - return _expirationDate > DateTime.Now; + return _expirationDate > DateTime.UtcNow; } } } \ No newline at end of file diff --git a/src/CheckoutSdk/OAuthSdkCredentials.cs b/src/CheckoutSdk/OAuthSdkCredentials.cs index fdb385dd..00a8cf32 100644 --- a/src/CheckoutSdk/OAuthSdkCredentials.cs +++ b/src/CheckoutSdk/OAuthSdkCredentials.cs @@ -13,7 +13,6 @@ public sealed class OAuthSdkCredentials : SdkCredentials #if (NETSTANDARD2_0_OR_GREATER || NETCOREAPP3_1_OR_GREATER) private readonly ILogger _log = LogProvider.GetLogger(typeof(OAuthSdkCredentials)); #endif - private readonly string _clientId; private readonly string _clientSecret; private readonly JsonSerializer _serializer = new JsonSerializer(); @@ -80,9 +79,9 @@ private OAuthServiceResponse Request() var httpRequest = new HttpRequestMessage(HttpMethod.Post, string.Empty); var data = new List> { + new KeyValuePair("grant_type", "client_credentials"), new KeyValuePair("client_id", _clientId), new KeyValuePair("client_secret", _clientSecret), - new KeyValuePair("grant_type", "client_credentials"), new KeyValuePair("scope", GetScopes()) }; httpRequest.Content = new FormUrlEncodedContent(data); diff --git a/src/CheckoutSdk/OAuthServiceResponse.cs b/src/CheckoutSdk/OAuthServiceResponse.cs index 9bdf8928..c2793dd9 100644 --- a/src/CheckoutSdk/OAuthServiceResponse.cs +++ b/src/CheckoutSdk/OAuthServiceResponse.cs @@ -4,13 +4,16 @@ public sealed class OAuthServiceResponse { public string AccessToken { get; set; } + public string TokenType { get; set; } + public long ExpiresIn { get; set; } public string Error { get; set; } - public bool IsValid() - { - return AccessToken != null && ExpiresIn != 0 && Error == null; - } + public bool IsValid() => + !string.IsNullOrWhiteSpace(AccessToken) && + !string.IsNullOrWhiteSpace(TokenType) && + ExpiresIn > 0 && + string.IsNullOrWhiteSpace(Error); } } \ No newline at end of file diff --git a/test/CheckoutSdkTest/OAuthAccessTokenTests.cs b/test/CheckoutSdkTest/OAuthAccessTokenTests.cs new file mode 100644 index 00000000..121cfa37 --- /dev/null +++ b/test/CheckoutSdkTest/OAuthAccessTokenTests.cs @@ -0,0 +1,38 @@ +using System; +using Xunit; + +namespace Checkout +{ + public class OAuthAccessTokenTests + { + [Fact] + public void ShouldReturnTrueAndTokenWhenResponseIsValid() + { + var response = new OAuthServiceResponse + { + AccessToken = "valid_token", + TokenType = "Bearer", + ExpiresIn = 3600 + }; + + var token = OAuthAccessToken.FromOAuthServiceResponse(response); + + Assert.NotNull(token); + Assert.Equal("valid_token", token.Token); + Assert.True(token.IsValid()); + } + + [Fact] + public void ShouldThrowExceptionWhenResponseIsInvalid() + { + var response = new OAuthServiceResponse + { + AccessToken = null, + TokenType = "Bearer", + ExpiresIn = 3600 + }; + + Assert.Throws(() => OAuthAccessToken.FromOAuthServiceResponse(response)); + } + } +} \ No newline at end of file diff --git a/test/CheckoutSdkTest/OAuthSdkCredentialsTests.cs b/test/CheckoutSdkTest/OAuthSdkCredentialsTests.cs new file mode 100644 index 00000000..1704552b --- /dev/null +++ b/test/CheckoutSdkTest/OAuthSdkCredentialsTests.cs @@ -0,0 +1,91 @@ +using Moq; +using Moq.Protected; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Checkout +{ + public class OAuthSdkCredentialsTests + { + private OAuthSdkCredentials CreateSdkCredentials(HttpResponseMessage mockResponse) + { + var mockHttpMessageHandler = new Mock(MockBehavior.Strict); + + mockHttpMessageHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(mockResponse) + .Verifiable(); + + var httpClient = new HttpClient(mockHttpMessageHandler.Object) + { + BaseAddress = new Uri("https://fake-auth.com") + }; + + var httpClientFactoryMock = new Mock(); + httpClientFactoryMock + .Setup(_ => _.CreateClient()) + .Returns(httpClient); + + return new OAuthSdkCredentials( + httpClientFactoryMock.Object, + new Uri("https://fake-auth.com"), + "test_client_id", + "test_client_secret", + new HashSet() + ); + } + + [Fact] + public void ShouldReturnAuthorizationHeaderWhenTokenIsValid() + { + using var mockResponse = new HttpResponseMessage(); + mockResponse.StatusCode = HttpStatusCode.OK; + mockResponse.Content = new StringContent( + "{\"access_token\": \"valid_token\", \"token_type\": \"Bearer\", \"expires_in\": 3600}", + Encoding.UTF8, + "application/json"); + var sdk = CreateSdkCredentials(mockResponse); + sdk.InitAccess(); + + var authorization = sdk.GetSdkAuthorization(SdkAuthorizationType.OAuth); + Assert.NotNull(authorization); + + string expectedHeader = $"Bearer valid_token"; + string actualHeader = authorization.GetAuthorizationHeader(); + + Assert.Equal(expectedHeader, actualHeader); + } + + [Fact] + public void ShouldThrowExceptionWhenApiReturnsError() + { + using var mockResponse = new HttpResponseMessage(); + mockResponse.StatusCode = HttpStatusCode.BadRequest; + mockResponse.Content = new StringContent( + "{\"error\": \"invalid_client\"}", + Encoding.UTF8, + "application/json"); + var sdk = CreateSdkCredentials(mockResponse); + + Assert.Throws(() => sdk.InitAccess()); + } + + [Fact] + public void ShouldThrowExceptionWhenResponseHasInvalidToken() + { + var response = new OAuthServiceResponse { AccessToken = null, TokenType = "Bearer", ExpiresIn = 3600 }; + + Assert.Throws(() => OAuthAccessToken.FromOAuthServiceResponse(response)); + } + } +} \ No newline at end of file diff --git a/test/CheckoutSdkTest/OAuthServiceResponseTests.cs b/test/CheckoutSdkTest/OAuthServiceResponseTests.cs new file mode 100644 index 00000000..374bd4bb --- /dev/null +++ b/test/CheckoutSdkTest/OAuthServiceResponseTests.cs @@ -0,0 +1,49 @@ +using Xunit; + +namespace Checkout +{ + public class OAuthServiceResponseTests + { + [Fact] + public void ShouldReturnTrueWhenResponseIsValid() + { + var response = new OAuthServiceResponse + { + AccessToken = "valid_token", + TokenType = "Bearer", + ExpiresIn = 3600, + Error = null + }; + + Assert.True(response.IsValid()); + } + + [Fact] + public void ShouldReturnFalseWhenResponseIsInvalid() + { + var response = new OAuthServiceResponse + { + AccessToken = null, + TokenType = "Bearer", + ExpiresIn = 3600, + Error = null + }; + + Assert.False(response.IsValid()); + } + + [Fact] + public void ShouldReturnFalseWhenExpiresInIsNegative() + { + var response = new OAuthServiceResponse + { + AccessToken = "valid_token", + TokenType = "Bearer", + ExpiresIn = -1, + Error = null + }; + + Assert.False(response.IsValid()); + } + } +} \ No newline at end of file