diff --git a/README.md b/README.md index 19aa45a7..67e7cb3b 100644 --- a/README.md +++ b/README.md @@ -139,11 +139,11 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( ## ⌛ Progress ### Server & Client -![15 / 288](https://progress-bar.dev/15/?scale=288&suffix=%20/%20288&width=500) +![16 / 288](https://progress-bar.dev/16/?scale=288&suffix=%20/%20288&width=500) ### Server Only ![2 / 195](https://progress-bar.dev/2/?scale=195&suffix=%20/%20195&width=300) ### Client Only -![13 / 93](https://progress-bar.dev/13/?scale=93&suffix=%20/%2093&width=300) +![14 / 93](https://progress-bar.dev/14/?scale=93&suffix=%20/%2093&width=300) ### 🔑 Key | Icon | Definition | @@ -153,7 +153,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( | ❌ | There is currently no intention to implement the endpoint for the given SDK type (client or server) | ### Account -![15 / 52](https://progress-bar.dev/15/?scale=52&suffix=%20/%2052&width=120) +![16 / 52](https://progress-bar.dev/16/?scale=52&suffix=%20/%2052&width=120) | Endpoint | Client | Server | |:-:|:-:|:-:| @@ -201,7 +201,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( | [Create OAuth2 Token](https://appwrite.io/docs/references/1.5.x/client-rest/account#createOAuth2Token) | ⬛ | ⬛ | | [Create Phone Token](https://appwrite.io/docs/references/1.5.x/client-rest/account#createPhoneToken) | ⬛ | ⬛ | | [Create Email Verification](https://appwrite.io/docs/references/1.5.x/client-rest/account#createVerification) | ✅ | ❌ | -| [Create Email Verification (Confirmation)](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateVerification) | ⬛ | ❌ | +| [Create Email Verification (Confirmation)](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateVerification) | ✅ | ❌ | | [Create Phone Verification](https://appwrite.io/docs/references/1.5.x/client-rest/account#createPhoneVerification) | ⬛ | ❌ | | [Create Phone Verification (Confirmation)](https://appwrite.io/docs/references/1.5.x/client-rest/account#updatePhoneVerification) | ⬛ | ❌ | diff --git a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs index 88e6bfe0..15f4cb27 100644 --- a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs @@ -246,4 +246,21 @@ public async Task> CreateEmailVerification(CreateEmailVeri return e.GetExceptionResponse(); } } + + /// + public async Task> CreateEmailVerificationConfirmation(CreateEmailVerificationConfirmationRequest request) + { + try + { + request.Validate(true); + + var result = await _accountApi.CreateEmailVerificationConfirmation(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 3d53e61b..bbac581b 100644 --- a/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs @@ -117,4 +117,12 @@ public interface IAccountClient /// The request content /// The token Task> CreateEmailVerification(CreateEmailVerificationRequest request); + + /// + /// Use this endpoint to complete the user email verification process. Use both the userId and secret parameters that were attached to your app URL to verify the user email ownership. If confirmed this route will return a 200 status code. + /// Appwrite Docs + /// + /// The request content + /// The token + Task> CreateEmailVerificationConfirmation(CreateEmailVerificationConfirmationRequest request); } diff --git a/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs b/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs index 2985dd38..d93e4851 100644 --- a/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs +++ b/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs @@ -44,6 +44,9 @@ internal interface IAccountApi : IBaseApi [Patch("/account/sessions/{sessionId}")] Task> UpdateSession([Header("x-appwrite-session")] string? session, string sessionId); - [Post(("/account/verification"))] + [Post("/account/verification")] Task> CreateEmailVerification([Header("x-appwrite-session")] string? session, CreateEmailVerificationRequest request); + + [Put("/account/verification")] + Task> CreateEmailVerificationConfirmation(CreateEmailVerificationConfirmationRequest request); } diff --git a/src/PinguApps.Appwrite.Playground/App.cs b/src/PinguApps.Appwrite.Playground/App.cs index f0d5173c..f1983854 100644 --- a/src/PinguApps.Appwrite.Playground/App.cs +++ b/src/PinguApps.Appwrite.Playground/App.cs @@ -28,6 +28,14 @@ public async Task Run(string[] args) var response = await _client.Account.CreateEmailVerification(request); + //var request = new CreateEmailVerificationConfirmationRequest + //{ + // Secret = "secret", + // UserId = "userId" + //}; + + //var response = await _client.Account.CreateEmailVerificationConfirmation(request); + Console.WriteLine(response.Result.Match( account => account.ToString(), appwriteError => appwriteError.Message, diff --git a/src/PinguApps.Appwrite.Shared/Requests/CreateEmailVerificationConfirmationRequest.cs b/src/PinguApps.Appwrite.Shared/Requests/CreateEmailVerificationConfirmationRequest.cs new file mode 100644 index 00000000..62c21f56 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Requests/CreateEmailVerificationConfirmationRequest.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Requests.Validators; + +namespace PinguApps.Appwrite.Shared.Requests; +public class CreateEmailVerificationConfirmationRequest : BaseRequest +{ + /// + /// User ID. + /// + [JsonPropertyName("userId")] + public string UserId { get; set; } = string.Empty; + + /// + /// Valid verification token. + /// + [JsonPropertyName("secret")] + public string Secret { get; set; } = string.Empty; +} diff --git a/src/PinguApps.Appwrite.Shared/Requests/Validators/CreateEmailVerificationConfirmationRequestValidator.cs b/src/PinguApps.Appwrite.Shared/Requests/Validators/CreateEmailVerificationConfirmationRequestValidator.cs new file mode 100644 index 00000000..ba49448b --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Requests/Validators/CreateEmailVerificationConfirmationRequestValidator.cs @@ -0,0 +1,11 @@ +using FluentValidation; + +namespace PinguApps.Appwrite.Shared.Requests.Validators; +public class CreateEmailVerificationConfirmationRequestValidator : AbstractValidator +{ + public CreateEmailVerificationConfirmationRequestValidator() + { + RuleFor(x => x.UserId).NotEmpty().Matches("^[a-zA-Z0-9][a-zA-Z0-9._-]{0,35}$"); + RuleFor(x => x.Secret).NotEmpty(); + } +} diff --git a/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateEmailVerificationConfirmation.cs b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateEmailVerificationConfirmation.cs new file mode 100644 index 00000000..4acef7d7 --- /dev/null +++ b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateEmailVerificationConfirmation.cs @@ -0,0 +1,77 @@ +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 CreateEmailVerificationConfirmation_ShouldReturnSuccess_WhenApiCallSucceeds() + { + // Arrange + var request = new CreateEmailVerificationConfirmationRequest() + { + UserId = "123456", + Secret = "654321" + }; + + _mockHttp.Expect(HttpMethod.Put, $"{Constants.Endpoint}/account/verification") + .ExpectedHeaders() + .WithJsonContent(request) + .Respond(Constants.AppJson, Constants.TokenResponse); + + // Act + var result = await _appwriteClient.Account.CreateEmailVerificationConfirmation(request); + + // Assert + Assert.True(result.Success); + } + + [Fact] + public async Task CreateEmailVerificationConfirmation_ShouldHandleException_WhenApiCallFails() + { + // Arrange + var request = new CreateEmailVerificationConfirmationRequest() + { + UserId = "123456", + Secret = "654321" + }; + + _mockHttp.Expect(HttpMethod.Put, $"{Constants.Endpoint}/account/verification") + .ExpectedHeaders() + .WithJsonContent(request) + .Respond(HttpStatusCode.BadRequest, Constants.AppJson, Constants.AppwriteError); + + // Act + var result = await _appwriteClient.Account.CreateEmailVerificationConfirmation(request); + + // Assert + Assert.True(result.IsError); + Assert.True(result.IsAppwriteError); + } + + [Fact] + public async Task CreateEmailVerificationConfirmation_ShouldReturnErrorResponse_WhenExceptionOccurs() + { + // Arrange + var request = new CreateEmailVerificationConfirmationRequest() + { + UserId = "123456", + Secret = "654321" + }; + + _mockHttp.Expect(HttpMethod.Put, $"{Constants.Endpoint}/account/verification") + .ExpectedHeaders() + .WithJsonContent(request) + .Throw(new HttpRequestException("An error occurred")); + + // Act + var result = await _appwriteClient.Account.CreateEmailVerificationConfirmation(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.Shared.Tests/Requests/CreateEmailVerificationRequestConfirmationTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Requests/CreateEmailVerificationRequestConfirmationTests.cs new file mode 100644 index 00000000..3781ee7a --- /dev/null +++ b/tests/PinguApps.Appwrite.Shared.Tests/Requests/CreateEmailVerificationRequestConfirmationTests.cs @@ -0,0 +1,107 @@ +using FluentValidation; +using PinguApps.Appwrite.Shared.Requests; + +namespace PinguApps.Appwrite.Shared.Tests.Requests; +public class CreateEmailVerificationRequestConfirmationTests +{ + [Fact] + public void Constructor_InitializesWithExpectedValues() + { + // Arrange & Act + var request = new CreateEmailVerificationConfirmationRequest(); + + // Assert + Assert.Equal(string.Empty, request.UserId); + Assert.Equal(string.Empty, request.Secret); + } + + [Fact] + public void Properties_CanBeSet() + { + var userId = "123456"; + var secret = "test@example.com"; + + // Arrange + var request = new CreateEmailVerificationConfirmationRequest(); + + // Act + request.UserId = userId; + request.Secret = secret; + + // Assert + Assert.Equal(userId, request.UserId); + Assert.Equal(secret, request.Secret); + } + + [Fact] + public void IsValid_WithValidData_ReturnsTrue() + { + // Arrange + var request = new CreateEmailVerificationConfirmationRequest + { + UserId = "123456", + Secret = "654321" + }; + + // Act + var isValid = request.IsValid(); + + // Assert + Assert.True(isValid); + } + + [Theory] + [InlineData("badChar^", "654321")] + [InlineData(".bad", "654321")] + [InlineData("_bad", "654321")] + [InlineData("-bad", "654321")] + [InlineData("", "654321")] + [InlineData("1234567890123456789012345678901234567", "654321")] + [InlineData("123456", "")] + public void IsValid_WithInvalidData_ReturnsFalse(string userId, string secret) + { + // Arrange + var request = new CreateEmailVerificationConfirmationRequest + { + UserId = userId, + Secret = secret + }; + + // Act + var isValid = request.IsValid(); + + // Assert + Assert.False(isValid); + } + + [Fact] + public void Validate_WithThrowOnFailuresTrue_ThrowsValidationExceptionOnFailure() + { + // Arrange + var request = new CreateEmailVerificationConfirmationRequest + { + UserId = ".badChar^", + Secret = "" + }; + + // Assert + Assert.Throws(() => request.Validate(true)); + } + + [Fact] + public void Validate_WithThrowOnFailuresFalse_ReturnsInvalidResultOnFailure() + { + // Arrange + var request = new CreateEmailVerificationConfirmationRequest + { + UserId = ".badChar^", + Secret = "" + }; + + // Act + var result = request.Validate(false); + + // Assert + Assert.False(result.IsValid); + } +}