diff --git a/README.md b/README.md index 5a675546..f5ca3125 100644 --- a/README.md +++ b/README.md @@ -138,14 +138,14 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( ``` ## ⌛ Progress - -![Server & Client - 21 / 288](https://img.shields.io/badge/Server_&_Client-21%20%2F%20288-red?style=for-the-badge) + +![Server & Client - 22 / 288](https://img.shields.io/badge/Server_&_Client-22%20%2F%20288-red?style=for-the-badge) ![Server - 2 / 195](https://img.shields.io/badge/Server-2%20%2F%20195-red?style=for-the-badge) - -![Client - 19 / 93](https://img.shields.io/badge/Client-19%20%2F%2093-red?style=for-the-badge) + +![Client - 20 / 93](https://img.shields.io/badge/Client-20%20%2F%2093-red?style=for-the-badge) ### 🔑 Key | Icon | Definition | @@ -155,8 +155,8 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( | ❌ | There is currently no intention to implement the endpoint for the given SDK type (client or server) | ### Account - -![Account - 21 / 52](https://img.shields.io/badge/Account-21%20%2F%2052-yellow?style=for-the-badge) + +![Account - 22 / 52](https://img.shields.io/badge/Account-22%20%2F%2052-yellow?style=for-the-badge) | Endpoint | Client | Server | |:-:|:-:|:-:| @@ -170,7 +170,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( | [Update MFA](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateMFA) | ✅ | ❌ | | [Add Authenticator](https://appwrite.io/docs/references/1.5.x/client-rest/account#createMfaAuthenticator) | ✅ | ❌ | | [Verify Authenticator](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateMfaAuthenticator) | ✅ | ❌ | -| [Delete Authenticator](https://appwrite.io/docs/references/1.5.x/client-rest/account#deleteMfaAuthenticator) | ⬛ | ❌ | +| [Delete Authenticator](https://appwrite.io/docs/references/1.5.x/client-rest/account#deleteMfaAuthenticator) | ✅ | ❌ | | [Create 2FA Challenge](https://appwrite.io/docs/references/1.5.x/client-rest/account#createMfaChallenge) | ⬛ | ❌ | | [Create MFA Challenge (confirmation)](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateMfaChallenge) | ⬛ | ❌ | | [List Factors](https://appwrite.io/docs/references/1.5.x/client-rest/account#listMfaFactors) | ⬛ | ❌ | diff --git a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs index 9a6ef10c..4ef2e742 100644 --- a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs @@ -299,11 +299,13 @@ public async Task> ListLogs(List? queries = null } /// - public async Task> AddAuthenticator(string type = "totp") + public async Task> AddAuthenticator(AddAuthenticatorRequest request) { try { - var result = await _accountApi.AddAuthenticator(Session, type); + request.Validate(true); + + var result = await _accountApi.AddAuthenticator(Session, request.Type); return result.GetApiResponse(); } @@ -314,13 +316,13 @@ public async Task> AddAuthenticator(string type = "totp" } /// - public async Task> VerifyAuthenticator(VerifyAuthenticatorRequest request, string type = "totp") + public async Task> VerifyAuthenticator(VerifyAuthenticatorRequest request) { try { request.Validate(true); - var result = await _accountApi.VerifyAuthenticator(Session, type, request); + var result = await _accountApi.VerifyAuthenticator(Session, request.Type, request); return result.GetApiResponse(); } @@ -346,4 +348,21 @@ public async Task> UpdateMfa(UpdateMfaRequest request) return e.GetExceptionResponse(); } } + + /// + public async Task DeleteAuthenticator(DeleteAuthenticatorRequest request) + { + try + { + request.Validate(true); + + var result = await _accountApi.DeleteAuthenticator(Session, request.Type, request); + + return result.GetApiResponse(); + } + catch (Exception e) + { + return e.GetExceptionResponse(); + } + } } diff --git a/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs b/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs index fa549def..e4c12988 100644 --- a/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs @@ -148,7 +148,7 @@ public interface IAccountClient /// /// Type of authenticator. Must be `totp` /// The MfaType - Task> AddAuthenticator(string type = "totp"); + Task> AddAuthenticator(AddAuthenticatorRequest request); /// /// Verify an authenticator app after adding it using . @@ -157,7 +157,7 @@ public interface IAccountClient /// The request content /// Type of authenticator /// The User - Task> VerifyAuthenticator(VerifyAuthenticatorRequest request, string type = "totp"); + Task> VerifyAuthenticator(VerifyAuthenticatorRequest request); /// /// Enable or disable MFA on an account @@ -166,4 +166,12 @@ public interface IAccountClient /// The request content /// The user Task> UpdateMfa(UpdateMfaRequest request); + + /// + /// Delete an authenticator for a user by ID + /// Appwrite Docs + /// + /// The request content + /// The result + Task DeleteAuthenticator(DeleteAuthenticatorRequest request); } diff --git a/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs b/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs index d4533158..f31f5aac 100644 --- a/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs +++ b/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs @@ -65,4 +65,7 @@ internal interface IAccountApi : IBaseApi [Patch("/account/mfa")] Task> UpdateMfa([Header("x-appwrite-session")] string? session, UpdateMfaRequest request); + + [Delete("/account/mfa/authenticators/{type}")] + Task DeleteAuthenticator([Header("x-appwrite-session")] string? session, string type, [Body] DeleteAuthenticatorRequest request); } diff --git a/src/PinguApps.Appwrite.Client/Utils/ResponseUtils.cs b/src/PinguApps.Appwrite.Client/Utils/ResponseUtils.cs index ecfeacc4..2980a6bd 100644 --- a/src/PinguApps.Appwrite.Client/Utils/ResponseUtils.cs +++ b/src/PinguApps.Appwrite.Client/Utils/ResponseUtils.cs @@ -1,11 +1,29 @@ using System; using System.Text.Json; +using OneOf.Types; using PinguApps.Appwrite.Shared; using Refit; namespace PinguApps.Appwrite.Client.Utils; internal static class ResponseUtils { + internal static AppwriteResult GetApiResponse(this IApiResponse result) + { + if (result.IsSuccessStatusCode) + { + return new AppwriteResult(new Success()); + } + + if (result.Error?.Content is null || string.IsNullOrEmpty(result.Error.Content)) + { + throw new Exception("Unknown error encountered."); + } + + var error = JsonSerializer.Deserialize(result.Error.Content); + + return new AppwriteResult(error!); + } + internal static AppwriteResult GetApiResponse(this IApiResponse result) { if (result.IsSuccessStatusCode) @@ -28,6 +46,11 @@ internal static AppwriteResult GetApiResponse(this IApiResponse result) return new AppwriteResult(error!); } + internal static AppwriteResult GetExceptionResponse(this Exception e) + { + return new AppwriteResult(new InternalError(e.Message)); + } + internal static AppwriteResult GetExceptionResponse(this Exception e) { return new AppwriteResult(new InternalError(e.Message)); diff --git a/src/PinguApps.Appwrite.Playground/App.cs b/src/PinguApps.Appwrite.Playground/App.cs index 41e9c111..e103434b 100644 --- a/src/PinguApps.Appwrite.Playground/App.cs +++ b/src/PinguApps.Appwrite.Playground/App.cs @@ -21,9 +21,10 @@ public async Task Run(string[] args) _client.SetSession(_session); //var response = await _client.Account.AddAuthenticator(); - var response = await _client.Account.UpdateMfa(new Shared.Requests.UpdateMfaRequest + var response = await _client.Account.DeleteAuthenticator(new Shared.Requests.DeleteAuthenticatorRequest { - MfaEnabled = false + Type = "totp", + Otp = "413526" }); Console.WriteLine(response.Result.Match( diff --git a/src/PinguApps.Appwrite.Server/Utils/ResponseUtils.cs b/src/PinguApps.Appwrite.Server/Utils/ResponseUtils.cs index a31c654a..1e7c3920 100644 --- a/src/PinguApps.Appwrite.Server/Utils/ResponseUtils.cs +++ b/src/PinguApps.Appwrite.Server/Utils/ResponseUtils.cs @@ -1,11 +1,29 @@ using System; using System.Text.Json; +using OneOf.Types; using PinguApps.Appwrite.Shared; using Refit; namespace PinguApps.Appwrite.Server.Utils; internal static class ResponseUtils { + internal static AppwriteResult GetApiResponse(this IApiResponse result) + { + if (result.IsSuccessStatusCode) + { + return new AppwriteResult(new Success()); + } + + if (result.Error?.Content is null || string.IsNullOrEmpty(result.Error.Content)) + { + throw new Exception("Unknown error encountered."); + } + + var error = JsonSerializer.Deserialize(result.Error.Content); + + return new AppwriteResult(error!); + } + internal static AppwriteResult GetApiResponse(this IApiResponse result) { if (result.IsSuccessStatusCode) @@ -28,6 +46,11 @@ internal static AppwriteResult GetApiResponse(this IApiResponse result) return new AppwriteResult(error!); } + internal static AppwriteResult GetExceptionResponse(this Exception e) + { + return new AppwriteResult(new InternalError(e.Message)); + } + internal static AppwriteResult GetExceptionResponse(this Exception e) { return new AppwriteResult(new InternalError(e.Message)); diff --git a/src/PinguApps.Appwrite.Shared/AppwriteResult.cs b/src/PinguApps.Appwrite.Shared/AppwriteResult.cs index d0a04fc5..08e77070 100644 --- a/src/PinguApps.Appwrite.Shared/AppwriteResult.cs +++ b/src/PinguApps.Appwrite.Shared/AppwriteResult.cs @@ -1,40 +1,44 @@ using OneOf; +using OneOf.Types; namespace PinguApps.Appwrite.Shared; /// /// The result of all API calls /// -/// the type of response expected on success -public class AppwriteResult +public class AppwriteResult { - public AppwriteResult(OneOf result) + public AppwriteResult(OneOf result) { Result = result; } + protected AppwriteResult() + { + } + /// - /// The result of making the API call. Can be , or depending on what happened + /// The result of making the API call. Can be , or depending on what happened /// - public OneOf Result { get; } + public OneOf Result { get; } /// /// Indicates the API call was successful /// - public bool Success => Result.IsT0; + public virtual bool Success => Result.IsT0; /// /// Indicates there is an error /// - public bool IsError => Result.IsT1 || Result.IsT2; + public virtual bool IsError => Result.IsT1 || Result.IsT2; /// /// Indicates that there was an error thrown within Appwrite /// - public bool IsAppwriteError => Result.IsT1; + public virtual bool IsAppwriteError => Result.IsT1; /// /// Indicates that there was an error thrown within the SDK /// - public bool IsInternalError => Result.IsT2; + public virtual bool IsInternalError => Result.IsT2; } diff --git a/src/PinguApps.Appwrite.Shared/AppwriteResultOfT.cs b/src/PinguApps.Appwrite.Shared/AppwriteResultOfT.cs new file mode 100644 index 00000000..95c2cff7 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/AppwriteResultOfT.cs @@ -0,0 +1,30 @@ +using OneOf; + +namespace PinguApps.Appwrite.Shared; + +/// +/// the type of response expected on success +public class AppwriteResult : AppwriteResult +{ + public AppwriteResult(OneOf result) + { + Result = result; + } + + /// + /// /// The result of making the API call. Can be , or depending on what happened + /// + public new OneOf Result { get; } + + /// + public override bool Success => Result.IsT0; + + /// + public override bool IsError => Result.IsT1 || Result.IsT2; + + /// + public override bool IsAppwriteError => Result.IsT1; + + /// + public override bool IsInternalError => Result.IsT2; +} diff --git a/src/PinguApps.Appwrite.Shared/Requests/AddAuthenticatorRequest.cs b/src/PinguApps.Appwrite.Shared/Requests/AddAuthenticatorRequest.cs new file mode 100644 index 00000000..0514306a --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Requests/AddAuthenticatorRequest.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Requests.Validators; + +namespace PinguApps.Appwrite.Shared.Requests; +public class AddAuthenticatorRequest : BaseRequest +{ + /// + /// Type of authenticator. Must be `totp` + /// + [JsonIgnore] + public string Type { get; set; } = "totp"; +} diff --git a/src/PinguApps.Appwrite.Shared/Requests/BaseRequest.cs b/src/PinguApps.Appwrite.Shared/Requests/BaseRequest.cs index 858a9d7b..f6dc8df1 100644 --- a/src/PinguApps.Appwrite.Shared/Requests/BaseRequest.cs +++ b/src/PinguApps.Appwrite.Shared/Requests/BaseRequest.cs @@ -6,8 +6,17 @@ public abstract class BaseRequest where TRequest : class where TValidator : IValidator, new() { + /// + /// True if the request object passes all validation + /// + /// Whether the request object is valid public bool IsValid() => Validate().IsValid; + /// + /// Attempts to validate the request object + /// + /// If true, throws an exception on failure + /// The result, showing any errors if applicable public ValidationResult Validate(bool throwOnFailures = false) { var validator = new TValidator(); diff --git a/src/PinguApps.Appwrite.Shared/Requests/DeleteAuthenticatorRequest.cs b/src/PinguApps.Appwrite.Shared/Requests/DeleteAuthenticatorRequest.cs new file mode 100644 index 00000000..21fd9b78 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Requests/DeleteAuthenticatorRequest.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Requests.Validators; + +namespace PinguApps.Appwrite.Shared.Requests; + +/// +/// The request for deleting an authenticator +/// +public class DeleteAuthenticatorRequest : BaseRequest +{ + /// + /// Type of authenticator + /// + [JsonIgnore] + public string Type { get; set; } = "totp"; + + /// + /// Valid verification token + /// + [JsonPropertyName("otp")] + public string Otp { get; set; } = string.Empty; +} diff --git a/src/PinguApps.Appwrite.Shared/Requests/Validators/AddAuthenticatorRequestValidator.cs b/src/PinguApps.Appwrite.Shared/Requests/Validators/AddAuthenticatorRequestValidator.cs new file mode 100644 index 00000000..b9a5a200 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Requests/Validators/AddAuthenticatorRequestValidator.cs @@ -0,0 +1,10 @@ +using FluentValidation; + +namespace PinguApps.Appwrite.Shared.Requests.Validators; +public class AddAuthenticatorRequestValidator : AbstractValidator +{ + public AddAuthenticatorRequestValidator() + { + RuleFor(x => x.Type).NotEmpty(); + } +} diff --git a/src/PinguApps.Appwrite.Shared/Requests/Validators/DeleteAuthenticatorRequestValidator.cs b/src/PinguApps.Appwrite.Shared/Requests/Validators/DeleteAuthenticatorRequestValidator.cs new file mode 100644 index 00000000..c5e71830 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Requests/Validators/DeleteAuthenticatorRequestValidator.cs @@ -0,0 +1,11 @@ +using FluentValidation; + +namespace PinguApps.Appwrite.Shared.Requests.Validators; +public class DeleteAuthenticatorRequestValidator : AbstractValidator +{ + public DeleteAuthenticatorRequestValidator() + { + RuleFor(x => x.Type).NotEmpty(); + RuleFor(x => x.Otp).NotEmpty(); + } +} diff --git a/src/PinguApps.Appwrite.Shared/Requests/Validators/VerifyAuthenticatorRequestValidator.cs b/src/PinguApps.Appwrite.Shared/Requests/Validators/VerifyAuthenticatorRequestValidator.cs index ebcc168b..2561daa9 100644 --- a/src/PinguApps.Appwrite.Shared/Requests/Validators/VerifyAuthenticatorRequestValidator.cs +++ b/src/PinguApps.Appwrite.Shared/Requests/Validators/VerifyAuthenticatorRequestValidator.cs @@ -6,5 +6,6 @@ public class VerifyAuthenticatorRequestValidator : AbstractValidator x.Otp).NotEmpty(); + RuleFor(x => x.Type).NotEmpty(); } } diff --git a/src/PinguApps.Appwrite.Shared/Requests/VerifyAuthenticatorRequest.cs b/src/PinguApps.Appwrite.Shared/Requests/VerifyAuthenticatorRequest.cs index 6f335680..ab33b59b 100644 --- a/src/PinguApps.Appwrite.Shared/Requests/VerifyAuthenticatorRequest.cs +++ b/src/PinguApps.Appwrite.Shared/Requests/VerifyAuthenticatorRequest.cs @@ -4,6 +4,15 @@ namespace PinguApps.Appwrite.Shared.Requests; public class VerifyAuthenticatorRequest : BaseRequest { + /// + /// Valid verification token + /// [JsonPropertyName("otp")] public string Otp { get; set; } = string.Empty; + + /// + /// Type of authenticator + /// + [JsonIgnore] + public string Type { get; set; } = "totp"; } diff --git a/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.AddAuthenticator.cs b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.AddAuthenticator.cs index 47b12a33..d358f5c6 100644 --- a/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.AddAuthenticator.cs +++ b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.AddAuthenticator.cs @@ -1,4 +1,5 @@ using System.Net; +using PinguApps.Appwrite.Shared.Requests; using PinguApps.Appwrite.Shared.Tests; using RichardSzalay.MockHttp; @@ -9,6 +10,8 @@ public partial class AccountClientTests public async Task AddAuthenticator_ShouldReturnSuccess_WhenApiCallSucceeds() { // Arrange + var request = new AddAuthenticatorRequest(); + _mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/mfa/authenticators/totp") .ExpectedHeaders(true) .Respond(Constants.AppJson, Constants.MfaTypeResponse); @@ -16,7 +19,7 @@ public async Task AddAuthenticator_ShouldReturnSuccess_WhenApiCallSucceeds() _appwriteClient.SetSession(Constants.Session); // Act - var result = await _appwriteClient.Account.AddAuthenticator(); + var result = await _appwriteClient.Account.AddAuthenticator(request); // Assert Assert.True(result.Success); @@ -27,19 +30,23 @@ public async Task AddAuthenticator_ShouldHitDifferentEndpoint_WhenNewTypeIsUsed( { // Arrange var type = "newAuth"; + var request = new AddAuthenticatorRequest() + { + Type = type + }; var requestUri = $"{Constants.Endpoint}/account/mfa/authenticators/{type}"; - var request = _mockHttp.Expect(HttpMethod.Post, requestUri) + var mockRequest = _mockHttp.Expect(HttpMethod.Post, requestUri) .ExpectedHeaders(true) .Respond(Constants.AppJson, Constants.MfaTypeResponse); _appwriteClient.SetSession(Constants.Session); // Act - var result = await _appwriteClient.Account.AddAuthenticator(type); + var result = await _appwriteClient.Account.AddAuthenticator(request); // Assert _mockHttp.VerifyNoOutstandingExpectation(); - var matches = _mockHttp.GetMatchCount(request); + var matches = _mockHttp.GetMatchCount(mockRequest); Assert.Equal(1, matches); } @@ -47,6 +54,8 @@ public async Task AddAuthenticator_ShouldHitDifferentEndpoint_WhenNewTypeIsUsed( public async Task AddAuthenticator_ShouldHandleException_WhenApiCallFails() { // Arrange + var request = new AddAuthenticatorRequest(); + _mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/mfa/authenticators/totp") .ExpectedHeaders(true) .Respond(HttpStatusCode.BadRequest, Constants.AppJson, Constants.AppwriteError); @@ -54,7 +63,7 @@ public async Task AddAuthenticator_ShouldHandleException_WhenApiCallFails() _appwriteClient.SetSession(Constants.Session); // Act - var result = await _appwriteClient.Account.AddAuthenticator(); + var result = await _appwriteClient.Account.AddAuthenticator(request); // Assert Assert.True(result.IsError); @@ -65,6 +74,8 @@ public async Task AddAuthenticator_ShouldHandleException_WhenApiCallFails() public async Task AddAuthenticator_ShouldReturnErrorResponse_WhenExceptionOccurs() { // Arrange + var request = new AddAuthenticatorRequest(); + _mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/mfa/authenticators/totp") .ExpectedHeaders(true) .Throw(new HttpRequestException("An error occurred")); @@ -72,7 +83,7 @@ public async Task AddAuthenticator_ShouldReturnErrorResponse_WhenExceptionOccurs _appwriteClient.SetSession(Constants.Session); // Act - var result = await _appwriteClient.Account.AddAuthenticator(); + var result = await _appwriteClient.Account.AddAuthenticator(request); // Assert Assert.False(result.Success); diff --git a/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.DeleteAuthenticator.cs b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.DeleteAuthenticator.cs new file mode 100644 index 00000000..0498c263 --- /dev/null +++ b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.DeleteAuthenticator.cs @@ -0,0 +1,107 @@ +using System.Net; +using PinguApps.Appwrite.Shared.Requests; +using PinguApps.Appwrite.Shared.Tests; +using RichardSzalay.MockHttp; + +namespace PinguApps.Appwrite.Client.Tests.Clients.Account; +public partial class AccountClientTests +{ + [Fact] + public async Task DeleteAuthenticator_ShouldReturnSuccess_WhenApiCallSucceeds() + { + // Arrange + var request = new DeleteAuthenticatorRequest() + { + Otp = "123456" + }; + + _mockHttp.Expect(HttpMethod.Delete, $"{Constants.Endpoint}/account/mfa/authenticators/totp") + .ExpectedHeaders(true) + .WithJsonContent(request) + .Respond(Constants.AppJson, Constants.MfaTypeResponse); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.DeleteAuthenticator(request); + + // Assert + Assert.True(result.Success); + } + + [Fact] + public async Task DeleteAuthenticator_ShouldHitDifferentEndpoint_WhenNewTypeIsUsed() + { + // Arrange + var type = "newAuth"; + var request = new DeleteAuthenticatorRequest() + { + Type = type, + Otp = "123456" + }; + var requestUri = $"{Constants.Endpoint}/account/mfa/authenticators/{type}"; + var mockRequest = _mockHttp.Expect(HttpMethod.Delete, requestUri) + .ExpectedHeaders(true) + .WithJsonContent(request) + .Respond(Constants.AppJson, Constants.MfaTypeResponse); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.DeleteAuthenticator(request); + + // Assert + _mockHttp.VerifyNoOutstandingExpectation(); + var matches = _mockHttp.GetMatchCount(mockRequest); + Assert.Equal(1, matches); + } + + [Fact] + public async Task DeleteAuthenticator_ShouldHandleException_WhenApiCallFails() + { + // Arrange + var request = new DeleteAuthenticatorRequest() + { + Otp = "123456" + }; + + _mockHttp.Expect(HttpMethod.Delete, $"{Constants.Endpoint}/account/mfa/authenticators/totp") + .ExpectedHeaders(true) + .WithJsonContent(request) + .Respond(HttpStatusCode.BadRequest, Constants.AppJson, Constants.AppwriteError); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.DeleteAuthenticator(request); + + // Assert + Assert.True(result.IsError); + Assert.True(result.IsAppwriteError); + } + + [Fact] + public async Task DeleteAuthenticator_ShouldReturnErrorResponse_WhenExceptionOccurs() + { + // Arrange + var request = new DeleteAuthenticatorRequest() + { + Otp = "123456" + }; + + _mockHttp.Expect(HttpMethod.Delete, $"{Constants.Endpoint}/account/mfa/authenticators/totp") + .ExpectedHeaders(true) + .WithJsonContent(request) + .Throw(new HttpRequestException("An error occurred")); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.DeleteAuthenticator(request); + + // Assert + Assert.False(result.Success); + Assert.True(result.IsInternalError); + Assert.Equal("An error occurred", result.Result.AsT2.Message); + } +} diff --git a/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.VerifyAuthenticator.cs b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.VerifyAuthenticator.cs index 7efca464..c10a34cc 100644 --- a/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.VerifyAuthenticator.cs +++ b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.VerifyAuthenticator.cs @@ -36,7 +36,8 @@ public async Task VerifyAuthenticator_ShouldHitDifferentEndpoint_WhenNewTypeIsUs var requestUri = $"{Constants.Endpoint}/account/mfa/authenticators/{type}"; var requestBody = new VerifyAuthenticatorRequest { - Otp = "123456" + Otp = "123456", + Type = type }; var request = _mockHttp.Expect(HttpMethod.Put, requestUri) .WithJsonContent(requestBody) @@ -46,7 +47,7 @@ public async Task VerifyAuthenticator_ShouldHitDifferentEndpoint_WhenNewTypeIsUs _appwriteClient.SetSession(Constants.Session); // Act - var result = await _appwriteClient.Account.VerifyAuthenticator(requestBody, type); + var result = await _appwriteClient.Account.VerifyAuthenticator(requestBody); // Assert _mockHttp.VerifyNoOutstandingExpectation(); diff --git a/tests/PinguApps.Appwrite.Client.Tests/Utils/ResponseUtilsTests.cs b/tests/PinguApps.Appwrite.Client.Tests/Utils/ResponseUtilsTests.cs index 31167695..07924728 100644 --- a/tests/PinguApps.Appwrite.Client.Tests/Utils/ResponseUtilsTests.cs +++ b/tests/PinguApps.Appwrite.Client.Tests/Utils/ResponseUtilsTests.cs @@ -1,5 +1,6 @@ using System.Net; using Moq; +using OneOf.Types; using PinguApps.Appwrite.Client.Utils; using PinguApps.Appwrite.Shared.Tests; using Refit; @@ -9,6 +10,60 @@ public class ResponseUtilsTests { [Fact] public void GetApiResponse_Success_ReturnsContent() + { + var mockApiResponse = new Mock(); + mockApiResponse.SetupGet(r => r.IsSuccessStatusCode).Returns(true); + + var result = mockApiResponse.Object.GetApiResponse(); + + Assert.True(result.Success); + Assert.IsType(result.Result.AsT0); + } + + [Fact] + public async Task GetApiResponse_Failure_ReturnsError() + { + var response = new HttpResponseMessage(HttpStatusCode.InternalServerError) + { + Content = new StringContent(Constants.AppwriteError) + }; + var exception = await ApiException.Create(new HttpRequestMessage(), HttpMethod.Get, response, new RefitSettings()); + + var mockApiResponse = new Mock(); + mockApiResponse.SetupGet(r => r.IsSuccessStatusCode).Returns(false); + mockApiResponse.SetupGet(x => x.Error).Returns(exception); + + var result = mockApiResponse.Object.GetApiResponse(); + + Assert.False(result.Success); + Assert.True(result.IsAppwriteError); + Assert.True(result.Result.IsT1); + } + + [Fact] + public async Task GetApiResponse_FailureButNullErrorContent_ThrowsException() + { + var exception = await ApiException.Create(new HttpRequestMessage(), HttpMethod.Get, new HttpResponseMessage(HttpStatusCode.InternalServerError), new RefitSettings()); + + var mockApiResponse = new Mock(); + mockApiResponse.SetupGet(r => r.IsSuccessStatusCode).Returns(false); + mockApiResponse.SetupGet(x => x.Error).Returns(exception); + + Assert.Throws(() => mockApiResponse.Object.GetApiResponse()); + } + + [Fact] + public void GetApiResponse_FailureButNullError_ThrowsException() + { + var mockApiResponse = new Mock(); + mockApiResponse.SetupGet(r => r.IsSuccessStatusCode).Returns(false); + mockApiResponse.SetupGet(x => x.Error).Returns((ApiException)null!); + + Assert.Throws(() => mockApiResponse.Object.GetApiResponse()); + } + + [Fact] + public void GenericGetApiResponse_Success_ReturnsContent() { var mockApiResponse = new Mock>(); mockApiResponse.SetupGet(r => r.IsSuccessStatusCode).Returns(true); @@ -21,7 +76,7 @@ public void GetApiResponse_Success_ReturnsContent() } [Fact] - public void GetApiResponse_SuccessButNullContent_ReturnsInternalError() + public void GenericGetApiResponse_SuccessButNullContent_ReturnsInternalError() { var mockApiResponse = new Mock>(); mockApiResponse.SetupGet(r => r.IsSuccessStatusCode).Returns(true); @@ -34,7 +89,7 @@ public void GetApiResponse_SuccessButNullContent_ReturnsInternalError() } [Fact] - public async Task GetApiResponse_Failure_ReturnsError() + public async Task GenericGetApiResponse_Failure_ReturnsError() { var response = new HttpResponseMessage(HttpStatusCode.InternalServerError) { @@ -54,7 +109,7 @@ public async Task GetApiResponse_Failure_ReturnsError() } [Fact] - public async Task GetApiResponse_FailureButNullErrorContent_ThrowsException() + public async Task GenericGetApiResponse_FailureButNullErrorContent_ThrowsException() { var exception = await ApiException.Create(new HttpRequestMessage(), HttpMethod.Get, new HttpResponseMessage(HttpStatusCode.InternalServerError), new RefitSettings()); @@ -66,7 +121,7 @@ public async Task GetApiResponse_FailureButNullErrorContent_ThrowsException() } [Fact] - public void GetApiResponse_FailureButNullError_ThrowsException() + public void GenericGetApiResponse_FailureButNullError_ThrowsException() { var mockApiResponse = new Mock>(); mockApiResponse.SetupGet(r => r.IsSuccessStatusCode).Returns(false); @@ -80,6 +135,19 @@ public void GetExceptionResponse_ReturnsInternalError() { var exception = new Exception("Test exception"); + var result = exception.GetExceptionResponse(); + + Assert.False(result.Success); + Assert.True(result.IsInternalError); + Assert.True(result.Result.IsT2); + Assert.Equal("Test exception", result.Result.AsT2.Message); + } + + [Fact] + public void GenericGetExceptionResponse_ReturnsInternalError() + { + var exception = new Exception("Test exception"); + var result = exception.GetExceptionResponse(); Assert.False(result.Success); diff --git a/tests/PinguApps.Appwrite.Server.Tests/Utils/ResponseUtilsTests.cs b/tests/PinguApps.Appwrite.Server.Tests/Utils/ResponseUtilsTests.cs index 5ddd5ad2..0cb55ad9 100644 --- a/tests/PinguApps.Appwrite.Server.Tests/Utils/ResponseUtilsTests.cs +++ b/tests/PinguApps.Appwrite.Server.Tests/Utils/ResponseUtilsTests.cs @@ -1,5 +1,6 @@ using System.Net; using Moq; +using OneOf.Types; using PinguApps.Appwrite.Server.Utils; using PinguApps.Appwrite.Shared.Tests; using Refit; @@ -9,6 +10,60 @@ public class ResponseUtilsTests { [Fact] public void GetApiResponse_Success_ReturnsContent() + { + var mockApiResponse = new Mock(); + mockApiResponse.SetupGet(r => r.IsSuccessStatusCode).Returns(true); + + var result = mockApiResponse.Object.GetApiResponse(); + + Assert.True(result.Success); + Assert.IsType(result.Result.AsT0); + } + + [Fact] + public async Task GetApiResponse_Failure_ReturnsError() + { + var response = new HttpResponseMessage(HttpStatusCode.InternalServerError) + { + Content = new StringContent(Constants.AppwriteError) + }; + var exception = await ApiException.Create(new HttpRequestMessage(), HttpMethod.Get, response, new RefitSettings()); + + var mockApiResponse = new Mock(); + mockApiResponse.SetupGet(r => r.IsSuccessStatusCode).Returns(false); + mockApiResponse.SetupGet(x => x.Error).Returns(exception); + + var result = mockApiResponse.Object.GetApiResponse(); + + Assert.False(result.Success); + Assert.True(result.IsAppwriteError); + Assert.True(result.Result.IsT1); + } + + [Fact] + public async Task GetApiResponse_FailureButNullErrorContent_ThrowsException() + { + var exception = await ApiException.Create(new HttpRequestMessage(), HttpMethod.Get, new HttpResponseMessage(HttpStatusCode.InternalServerError), new RefitSettings()); + + var mockApiResponse = new Mock(); + mockApiResponse.SetupGet(r => r.IsSuccessStatusCode).Returns(false); + mockApiResponse.SetupGet(x => x.Error).Returns(exception); + + Assert.Throws(() => mockApiResponse.Object.GetApiResponse()); + } + + [Fact] + public void GetApiResponse_FailureButNullError_ThrowsException() + { + var mockApiResponse = new Mock(); + mockApiResponse.SetupGet(r => r.IsSuccessStatusCode).Returns(false); + mockApiResponse.SetupGet(x => x.Error).Returns((ApiException)null!); + + Assert.Throws(() => mockApiResponse.Object.GetApiResponse()); + } + + [Fact] + public void GenericGetApiResponse_Success_ReturnsContent() { var mockApiResponse = new Mock>(); mockApiResponse.SetupGet(r => r.IsSuccessStatusCode).Returns(true); @@ -21,7 +76,7 @@ public void GetApiResponse_Success_ReturnsContent() } [Fact] - public void GetApiResponse_SuccessButNullContent_ReturnsInternalError() + public void GenericGetApiResponse_SuccessButNullContent_ReturnsInternalError() { var mockApiResponse = new Mock>(); mockApiResponse.SetupGet(r => r.IsSuccessStatusCode).Returns(true); @@ -34,7 +89,7 @@ public void GetApiResponse_SuccessButNullContent_ReturnsInternalError() } [Fact] - public async Task GetApiResponse_Failure_ReturnsError() + public async Task GenericGetApiResponse_Failure_ReturnsError() { var response = new HttpResponseMessage(HttpStatusCode.InternalServerError) { @@ -54,7 +109,7 @@ public async Task GetApiResponse_Failure_ReturnsError() } [Fact] - public async Task GetApiResponse_FailureButNullErrorContent_ThrowsException() + public async Task GenericGetApiResponse_FailureButNullErrorContent_ThrowsException() { var exception = await ApiException.Create(new HttpRequestMessage(), HttpMethod.Get, new HttpResponseMessage(HttpStatusCode.InternalServerError), new RefitSettings()); @@ -65,11 +120,34 @@ public async Task GetApiResponse_FailureButNullErrorContent_ThrowsException() Assert.Throws(() => mockApiResponse.Object.GetApiResponse()); } + [Fact] + public void GenericGetApiResponse_FailureButNullError_ThrowsException() + { + var mockApiResponse = new Mock>(); + mockApiResponse.SetupGet(r => r.IsSuccessStatusCode).Returns(false); + mockApiResponse.SetupGet(x => x.Error).Returns((ApiException)null!); + + Assert.Throws(() => mockApiResponse.Object.GetApiResponse()); + } + [Fact] public void GetExceptionResponse_ReturnsInternalError() { var exception = new Exception("Test exception"); + var result = exception.GetExceptionResponse(); + + Assert.False(result.Success); + Assert.True(result.IsInternalError); + Assert.True(result.Result.IsT2); + Assert.Equal("Test exception", result.Result.AsT2.Message); + } + + [Fact] + public void GenericGetExceptionResponse_ReturnsInternalError() + { + var exception = new Exception("Test exception"); + var result = exception.GetExceptionResponse(); Assert.False(result.Success); diff --git a/tests/PinguApps.Appwrite.Shared.Tests/AppwriteResultTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/AppwriteResultTests.cs index bfb6d3f1..6e5cb80d 100644 --- a/tests/PinguApps.Appwrite.Shared.Tests/AppwriteResultTests.cs +++ b/tests/PinguApps.Appwrite.Shared.Tests/AppwriteResultTests.cs @@ -1,4 +1,5 @@ using OneOf; +using OneOf.Types; namespace PinguApps.Appwrite.Shared.Tests; @@ -7,28 +8,65 @@ public class AppwriteResultTests [Fact] public void Constructor_WithTResult_SuccessIsTrue() { - var result = new AppwriteResult(OneOf.FromT0("Success")); + var result = new AppwriteResult(OneOf.FromT0(new Success())); Assert.True(result.Success); Assert.False(result.IsError); Assert.False(result.IsAppwriteError); Assert.False(result.IsInternalError); - Assert.True(result.Result.AsT0 == "Success"); + Assert.IsType(result.Result.AsT0); } [Fact] public void Constructor_WithAppwriteError_IsAppwriteErrorIsTrue() { - var result = new AppwriteResult(OneOf.FromT1(new AppwriteError("Message", 500, "Type", "Version"))); + var result = new AppwriteResult(OneOf.FromT1(new AppwriteError("Message", 500, "Type", "Version"))); Assert.False(result.Success); Assert.True(result.IsError); Assert.True(result.IsAppwriteError); Assert.False(result.IsInternalError); + Assert.Equal("Message", result.Result.AsT1.Message); } [Fact] public void Constructor_WithInternalError_IsInternalErrorIsTrue() + { + var result = new AppwriteResult(OneOf.FromT2(new InternalError("Message"))); + + Assert.False(result.Success); + Assert.True(result.IsError); + Assert.False(result.IsAppwriteError); + Assert.True(result.IsInternalError); + Assert.Equal("Message", result.Result.AsT2.Message); + } + + [Fact] + public void GenericConstructor_WithTResult_SuccessIsTrue() + { + var result = new AppwriteResult(OneOf.FromT0("Success")); + + Assert.True(result.Success); + Assert.False(result.IsError); + Assert.False(result.IsAppwriteError); + Assert.False(result.IsInternalError); + Assert.Equal("Success", result.Result.AsT0); + } + + [Fact] + public void GenericConstructor_WithAppwriteError_IsAppwriteErrorIsTrue() + { + var result = new AppwriteResult(OneOf.FromT1(new AppwriteError("Message", 500, "Type", "Version"))); + + Assert.False(result.Success); + Assert.True(result.IsError); + Assert.True(result.IsAppwriteError); + Assert.False(result.IsInternalError); + Assert.Equal("Message", result.Result.AsT1.Message); + } + + [Fact] + public void GenericConstructor_WithInternalError_IsInternalErrorIsTrue() { var result = new AppwriteResult(OneOf.FromT2(new InternalError("Message"))); @@ -36,5 +74,6 @@ public void Constructor_WithInternalError_IsInternalErrorIsTrue() Assert.True(result.IsError); Assert.False(result.IsAppwriteError); Assert.True(result.IsInternalError); + Assert.Equal("Message", result.Result.AsT2.Message); } } diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Requests/AddAuthenticatorRequestTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Requests/AddAuthenticatorRequestTests.cs new file mode 100644 index 00000000..96ef335a --- /dev/null +++ b/tests/PinguApps.Appwrite.Shared.Tests/Requests/AddAuthenticatorRequestTests.cs @@ -0,0 +1,95 @@ +using FluentValidation; +using PinguApps.Appwrite.Shared.Requests; + +namespace PinguApps.Appwrite.Shared.Tests.Requests; +public class AddAuthenticatorRequestTests +{ + [Fact] + public void Constructor_InitializesWithExpectedValues() + { + // Arrange & Act + var request = new AddAuthenticatorRequest(); + + // Assert + Assert.Equal("totp", request.Type); + } + + [Theory] + [InlineData("A string")] + [InlineData("123456")] + [InlineData("A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. ")] + public void Properties_CanBeSet(string type) + { + // Arrange + var request = new AddAuthenticatorRequest(); + + // Act + request.Type = type; + + // Assert + Assert.Equal(type, request.Type); + } + + [Fact] + public void IsValid_WithValidInputs_ReturnsTrue() + { + // Arrange + var request = new AddAuthenticatorRequest + { + Type = "abcd" + }; + + // Act + bool isValid = request.IsValid(); + + // Assert + Assert.True(isValid); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void IsValid_WithInvalidInputs_ReturnsFalse(string? type) + { + // Arrange + var request = new AddAuthenticatorRequest + { + Type = type! + }; + + // Act + var isValid = request.IsValid(); + + // Assert + Assert.False(isValid); + } + + [Fact] + public void Validate_WithThrowOnFailuresTrue_ThrowsValidationExceptionOnFailure() + { + // Arrange + var request = new AddAuthenticatorRequest + { + Type = null! + }; + + // Assert + Assert.Throws(() => request.Validate(true)); + } + + [Fact] + public void Validate_WithThrowOnFailuresFalse_ReturnsInvalidResultOnFailure() + { + // Arrange + var request = new AddAuthenticatorRequest + { + Type = null! + }; + + // Act + var result = request.Validate(false); + + // Assert + Assert.False(result.IsValid); + } +} diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Requests/DeleteAuthenticatorRequestTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Requests/DeleteAuthenticatorRequestTests.cs new file mode 100644 index 00000000..c5bf4f88 --- /dev/null +++ b/tests/PinguApps.Appwrite.Shared.Tests/Requests/DeleteAuthenticatorRequestTests.cs @@ -0,0 +1,103 @@ +using FluentValidation; +using PinguApps.Appwrite.Shared.Requests; + +namespace PinguApps.Appwrite.Shared.Tests.Requests; +public class DeleteAuthenticatorRequestTests +{ + [Fact] + public void Constructor_InitializesWithExpectedValues() + { + // Arrange & Act + var request = new DeleteAuthenticatorRequest(); + + // Assert + Assert.Equal("totp", request.Type); + Assert.Equal(string.Empty, request.Otp); + } + + [Theory] + [InlineData("A string", "A string")] + [InlineData("123456", "123456")] + [InlineData("A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. ", "A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. A much longer string. ")] + public void Properties_CanBeSet(string type, string otp) + { + // Arrange + var request = new DeleteAuthenticatorRequest(); + + // Act + request.Type = type; + request.Otp = otp; + + // Assert + Assert.Equal(type, request.Type); + } + + [Fact] + public void IsValid_WithValidInputs_ReturnsTrue() + { + // Arrange + var request = new DeleteAuthenticatorRequest + { + Type = "abcd", + Otp = "123456" + }; + + // Act + var isValid = request.IsValid(); + + // Assert + Assert.True(isValid); + } + + [Theory] + [InlineData("", "123456")] + [InlineData(null, "123456")] + [InlineData("abcd", "")] + [InlineData("abcd", null)] + public void IsValid_WithInvalidInputs_ReturnsFalse(string? type, string? otp) + { + // Arrange + var request = new DeleteAuthenticatorRequest + { + Type = type!, + Otp = otp! + }; + + // Act + var isValid = request.IsValid(); + + // Assert + Assert.False(isValid); + } + + [Fact] + public void Validate_WithThrowOnFailuresTrue_ThrowsValidationExceptionOnFailure() + { + // Arrange + var request = new DeleteAuthenticatorRequest + { + Type = null!, + Otp = null! + }; + + // Assert + Assert.Throws(() => request.Validate(true)); + } + + [Fact] + public void Validate_WithThrowOnFailuresFalse_ReturnsInvalidResultOnFailure() + { + // Arrange + var request = new DeleteAuthenticatorRequest + { + Type = null!, + Otp = null! + }; + + // Act + var result = request.Validate(false); + + // Assert + Assert.False(result.IsValid); + } +}