diff --git a/README.md b/README.md index 5cd9fea4..5cce3df8 100644 --- a/README.md +++ b/README.md @@ -138,14 +138,14 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( ``` ## ⌛ Progress - -![Server & Client - 33 / 288](https://img.shields.io/badge/Server_&_Client-33%20%2F%20288-red?style=for-the-badge) + +![Server & Client - 34 / 288](https://img.shields.io/badge/Server_&_Client-34%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 - 31 / 93](https://img.shields.io/badge/Client-31%20%2F%2093-red?style=for-the-badge) + +![Client - 32 / 93](https://img.shields.io/badge/Client-32%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 - 33 / 52](https://img.shields.io/badge/Account-33%20%2F%2052-yellow?style=for-the-badge) + +![Account - 34 / 52](https://img.shields.io/badge/Account-34%20%2F%2052-yellow?style=for-the-badge) | Endpoint | Client | Server | |:-:|:-:|:-:| @@ -187,7 +187,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( | [List Sessions](https://appwrite.io/docs/references/1.5.x/client-rest/account#listSessions) | ✅ | ❌ | | [Delete Sessions](https://appwrite.io/docs/references/1.5.x/client-rest/account#deleteSessions) | ✅ | ❌ | | [Create Anonymous Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#createAnonymousSession) | ✅ | ❌ | -| [Create Email Password Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#createEmailPasswordSession) | ⬛ | ❌ | +| [Create Email Password Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#createEmailPasswordSession) | ✅ | ❌ | | [Update Magic URL Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateMagicURLSession) | ⬛ | ❌ | | [Create OAuth2 Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#createOAuth2Session) | ⬛ | ❌ | | [Update Phone Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#updatePhoneSession) | ⬛ | ❌ | diff --git a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs index e141309e..9f70f532 100644 --- a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs @@ -544,4 +544,21 @@ public async Task> CreateAnonymousSession() return e.GetExceptionResponse(); } } + + /// + public async Task> CreateEmailPasswordSession(CreateEmailPasswordSessionRequest request) + { + try + { + request.Validate(true); + + var result = await _accountApi.CreateEmailPasswordSession(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 51fa5b4b..5bda3445 100644 --- a/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs @@ -256,4 +256,13 @@ public interface IAccountClient /// /// The Session Task> CreateAnonymousSession(); + + /// + /// Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user. + /// A user is limited to 10 active sessions at a time by default. Learn more about session limits. + /// Appwrite Docs + /// + /// The request + /// The Session + Task> CreateEmailPasswordSession(CreateEmailPasswordSessionRequest request); } diff --git a/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs b/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs index cc2b81a2..5f2142c6 100644 --- a/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs +++ b/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs @@ -101,4 +101,7 @@ internal interface IAccountApi : IBaseApi [Post("/account/sessions/anonymous")] Task> CreateAnonymousSession(); + + [Post("/account/sessions/email")] + Task> CreateEmailPasswordSession(CreateEmailPasswordSessionRequest request); } diff --git a/src/PinguApps.Appwrite.Playground/App.cs b/src/PinguApps.Appwrite.Playground/App.cs index 6c1bdd6b..eeae6a35 100644 --- a/src/PinguApps.Appwrite.Playground/App.cs +++ b/src/PinguApps.Appwrite.Playground/App.cs @@ -22,7 +22,11 @@ public async Task Run(string[] args) Console.WriteLine(_client.Session); - var response = await _client.Account.CreateAnonymousSession(); + var response = await _client.Account.CreateEmailPasswordSession(new Shared.Requests.CreateEmailPasswordSessionRequest + { + Email = "pingu@example.com", + Password = "password" + }); Console.WriteLine(response.Result.Match( account => account.ToString(), diff --git a/src/PinguApps.Appwrite.Shared/Requests/CreateEmailPasswordSessionRequest.cs b/src/PinguApps.Appwrite.Shared/Requests/CreateEmailPasswordSessionRequest.cs new file mode 100644 index 00000000..6ba87829 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Requests/CreateEmailPasswordSessionRequest.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Requests.Validators; + +namespace PinguApps.Appwrite.Shared.Requests; + +/// +/// The request for creating an email password session +/// +public class CreateEmailPasswordSessionRequest : BaseRequest +{ + /// + /// User email + /// + [JsonPropertyName("email")] + public string Email { get; set; } = string.Empty; + + /// + /// User password. Must be at least 8 chars + /// + [JsonPropertyName("password")] + public string Password { get; set; } = string.Empty; +} diff --git a/src/PinguApps.Appwrite.Shared/Requests/Validators/CreateEmailPasswordSessionRequestValidator.cs b/src/PinguApps.Appwrite.Shared/Requests/Validators/CreateEmailPasswordSessionRequestValidator.cs new file mode 100644 index 00000000..34c7cd9d --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Requests/Validators/CreateEmailPasswordSessionRequestValidator.cs @@ -0,0 +1,11 @@ +using FluentValidation; + +namespace PinguApps.Appwrite.Shared.Requests.Validators; +public class CreateEmailPasswordSessionRequestValidator : AbstractValidator +{ + public CreateEmailPasswordSessionRequestValidator() + { + RuleFor(x => x.Email).NotEmpty().EmailAddress(); + RuleFor(x => x.Password).NotEmpty().MinimumLength(8).MaximumLength(256); + } +} diff --git a/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateEmailPasswordSession.cs b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateEmailPasswordSession.cs new file mode 100644 index 00000000..c237f09d --- /dev/null +++ b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateEmailPasswordSession.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 CreateEmailPasswordSession_ShouldReturnSuccess_WhenApiCallSucceeds() + { + // Arrange + var request = new CreateEmailPasswordSessionRequest() + { + Email = "email@example.com", + Password = "Password" + }; + + _mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/sessions/email") + .ExpectedHeaders() + .WithJsonContent(request) + .Respond(Constants.AppJson, Constants.SessionResponse); + + // Act + var result = await _appwriteClient.Account.CreateEmailPasswordSession(request); + + // Assert + Assert.True(result.Success); + } + + [Fact] + public async Task CreateEmailPasswordSession_ShouldHandleException_WhenApiCallFails() + { + // Arrange + var request = new CreateEmailPasswordSessionRequest() + { + Email = "email@example.com", + Password = "Password" + }; + + _mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/sessions/email") + .ExpectedHeaders() + .WithJsonContent(request) + .Respond(HttpStatusCode.BadRequest, Constants.AppJson, Constants.AppwriteError); + + // Act + var result = await _appwriteClient.Account.CreateEmailPasswordSession(request); + + // Assert + Assert.True(result.IsError); + Assert.True(result.IsAppwriteError); + } + + [Fact] + public async Task CreateEmailPasswordSession_ShouldReturnErrorResponse_WhenExceptionOccurs() + { + // Arrange + var request = new CreateEmailPasswordSessionRequest() + { + Email = "email@example.com", + Password = "Password" + }; + + _mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/sessions/email") + .ExpectedHeaders() + .WithJsonContent(request) + .Throw(new HttpRequestException("An error occurred")); + + // Act + var result = await _appwriteClient.Account.CreateEmailPasswordSession(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/CreateEmailPasswordSessionRequestTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Requests/CreateEmailPasswordSessionRequestTests.cs new file mode 100644 index 00000000..8df631ba --- /dev/null +++ b/tests/PinguApps.Appwrite.Shared.Tests/Requests/CreateEmailPasswordSessionRequestTests.cs @@ -0,0 +1,107 @@ +using FluentValidation; +using PinguApps.Appwrite.Shared.Requests; + +namespace PinguApps.Appwrite.Shared.Tests.Requests; +public class CreateEmailPasswordSessionRequestTests +{ + [Fact] + public void Constructor_InitializesWithExpectedValues() + { + // Arrange & Act + var request = new CreateEmailPasswordSessionRequest(); + + // Assert + Assert.Equal(string.Empty, request.Email); + Assert.Equal(string.Empty, request.Password); + } + + [Theory] + [InlineData("test@example.com", "password123")] + [InlineData("another@example.com", "diffPassword")] + public void Properties_CanBeSet(string email, string password) + { + // Arrange + var request = new CreateEmailPasswordSessionRequest(); + + // Act + request.Email = email; + request.Password = password; + + // Assert + Assert.Equal(email, request.Email); + Assert.Equal(password, request.Password); + } + + [Theory] + [InlineData("pingu@example.com", "Password")] + public void IsValid_WithValidData_ReturnsTrue(string email, string password) + { + // Arrange + var request = new CreateEmailPasswordSessionRequest + { + Email = email, + Password = password + }; + + // Act + var isValid = request.IsValid(); + + // Assert + Assert.True(isValid); + } + + [Theory] + [InlineData(null, "Password")] + [InlineData("", "Password")] + [InlineData("not an email", "Password")] + [InlineData("pingu@example.com", null)] + [InlineData("pingu@example.com", "")] + [InlineData("pingu@example.com", "short")] + [InlineData("pingu@example.com", "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 IsValid_WithInvalidData_ReturnsFalse(string? email, string? password) + { + // Arrange + var request = new CreateEmailPasswordSessionRequest + { + Email = email!, + Password = password! + }; + + // Act + var isValid = request.IsValid(); + + // Assert + Assert.False(isValid); + } + + [Fact] + public void Validate_WithThrowOnFailuresTrue_ThrowsValidationExceptionOnFailure() + { + // Arrange + var request = new CreateEmailPasswordSessionRequest + { + Email = "not an email", + Password = "short" + }; + + // Assert + Assert.Throws(() => request.Validate(true)); + } + + [Fact] + public void Validate_WithThrowOnFailuresFalse_ReturnsInvalidResultOnFailure() + { + // Arrange + var request = new CreateEmailPasswordSessionRequest + { + Email = "not an email", + Password = "short" + }; + + // Act + var result = request.Validate(false); + + // Assert + Assert.False(result.IsValid); + } +}