From d96689265ebaaa41daa699aed395d8a878193e02 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Sun, 15 Sep 2024 17:42:03 +0100 Subject: [PATCH] 105 delete session (#156) * Add shared request for delete session * bump response format to 1.6 * implemented delete session on client * added shared tests * bump expected appwrite response header * Add client tests * Update README.md * [CodeFactor] Apply fixes --------- Co-authored-by: codefactor-io --- README.md | 14 +-- .../Clients/AccountClient.cs | 17 +++ .../Clients/IAccountClient.cs | 8 ++ .../Internals/IAccountApi.cs | 3 + .../Internals/IBaseApi.cs | 2 +- src/PinguApps.Appwrite.Playground/App.cs | 24 ++-- .../Internals/IBaseApi.cs | 2 +- .../Requests/DeleteSessionRequest.cs | 16 +++ .../DeleteSessionRequestValidator.cs | 10 ++ .../AccountClientTests.DeleteSession.cs | 109 ++++++++++++++++++ .../Constants.cs | 2 +- .../Requests/DeleteSessionRequestTests.cs | 93 +++++++++++++++ 12 files changed, 282 insertions(+), 18 deletions(-) create mode 100644 src/PinguApps.Appwrite.Shared/Requests/DeleteSessionRequest.cs create mode 100644 src/PinguApps.Appwrite.Shared/Requests/Validators/DeleteSessionRequestValidator.cs create mode 100644 tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.DeleteSession.cs create mode 100644 tests/PinguApps.Appwrite.Shared.Tests/Requests/DeleteSessionRequestTests.cs diff --git a/README.md b/README.md index c1614abe..a003a73e 100644 --- a/README.md +++ b/README.md @@ -138,14 +138,14 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( ``` ## βŒ› Progress - -![Server & Client - 38 / 293](https://img.shields.io/badge/Server_&_Client-38%20%2F%20293-red?style=for-the-badge) + +![Server & Client - 39 / 293](https://img.shields.io/badge/Server_&_Client-39%20%2F%20293-red?style=for-the-badge) ![Server - 5 / 200](https://img.shields.io/badge/Server-5%20%2F%20200-red?style=for-the-badge) - -![Client - 33 / 93](https://img.shields.io/badge/Client-33%20%2F%2093-red?style=for-the-badge) + +![Client - 34 / 93](https://img.shields.io/badge/Client-34%20%2F%2093-gold?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 - 38 / 57](https://img.shields.io/badge/Account-38%20%2F%2057-forestgreen?style=for-the-badge) + +![Account - 39 / 57](https://img.shields.io/badge/Account-39%20%2F%2057-forestgreen?style=for-the-badge) | Endpoint | Client | Server | |:-:|:-:|:-:| @@ -194,7 +194,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( | [Create Session](https://appwrite.io/docs/references/1.6.x/client-rest/account#createSession) | βœ… | βœ… | | [Get Session](https://appwrite.io/docs/references/1.6.x/client-rest/account#getSession) | βœ… | ❌ | | [Update Session](https://appwrite.io/docs/references/1.6.x/client-rest/account#updateSession) | βœ… | ❌ | -| [Delete Session](https://appwrite.io/docs/references/1.6.x/client-rest/account#deleteSession) | ⬛ | ❌ | +| [Delete Session](https://appwrite.io/docs/references/1.6.x/client-rest/account#deleteSession) | βœ… | ❌ | | [Update Status](https://appwrite.io/docs/references/1.6.x/client-rest/account#updateStatus) | ⬛ | ❌ | | [Create Push Target](https://appwrite.io/docs/references/1.6.x/client-rest/account#createPushTarget) | ⬛ | ❌ | | [Update Push Target](https://appwrite.io/docs/references/1.6.x/client-rest/account#updatePushTarget) | ⬛ | ❌ | diff --git a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs index e905eebd..fe36b414 100644 --- a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs @@ -580,4 +580,21 @@ public AppwriteResult CreateOauth2Session(CreateOauth2Sessi return e.GetExceptionResponse(); } } + + /// + public async Task DeleteSession(DeleteSessionRequest request) + { + try + { + request.Validate(true); + + var result = await _accountApi.DeleteSession(GetCurrentSessionOrThrow(), request.SessionId); + + 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 a222a6b8..f1034049 100644 --- a/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs @@ -275,4 +275,12 @@ public interface IAccountClient /// The request content /// The CreateOauth2Session object AppwriteResult CreateOauth2Session(CreateOauth2SessionRequest request); + + /// + /// Logout the user. Use 'current' as the session ID to logout on this device, use a session ID to logout on another device. If you're looking to logout the user on all devices, use instead. + /// Appwrite Docs + /// + /// The request content + /// code 204 for success + Task DeleteSession(DeleteSessionRequest request); } diff --git a/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs b/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs index 5f2142c6..859f56b3 100644 --- a/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs +++ b/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs @@ -104,4 +104,7 @@ internal interface IAccountApi : IBaseApi [Post("/account/sessions/email")] Task> CreateEmailPasswordSession(CreateEmailPasswordSessionRequest request); + + [Delete("/account/sessions/{sessionId}")] + Task DeleteSession([Header("x-appwrite-session")] string session, string sessionId); } diff --git a/src/PinguApps.Appwrite.Client/Internals/IBaseApi.cs b/src/PinguApps.Appwrite.Client/Internals/IBaseApi.cs index 984f68f6..07c99bce 100644 --- a/src/PinguApps.Appwrite.Client/Internals/IBaseApi.cs +++ b/src/PinguApps.Appwrite.Client/Internals/IBaseApi.cs @@ -7,7 +7,7 @@ namespace PinguApps.Appwrite.Client.Internals; "x-sdk-platform: client", "x-sdk-language: dotnet", "x-sdk-version: 0.0.1", - "X-Appwrite-Response-Format: 1.5.0")] + "X-Appwrite-Response-Format: 1.6.0")] internal interface IBaseApi { } diff --git a/src/PinguApps.Appwrite.Playground/App.cs b/src/PinguApps.Appwrite.Playground/App.cs index f857ef82..048fb42e 100644 --- a/src/PinguApps.Appwrite.Playground/App.cs +++ b/src/PinguApps.Appwrite.Playground/App.cs @@ -19,19 +19,27 @@ public App(IAppwriteClient client, IAppwriteServer server, IConfiguration config public async Task Run(string[] args) { - //_client.SetSession(_session); + _client.SetSession(_session); - var request = new CreateSessionRequest() - { - UserId = "664aac1a00113f82e620", - Secret = "339597" - }; - - var response = await _server.Account.CreateSession(request); + var response = await _client.Account.Get(); Console.WriteLine(response.Result.Match( account => account.ToString(), appwriteError => appwriteError.Message, internalERror => internalERror.Message)); + + var r2 = await _client.Account.DeleteSession(new DeleteSessionRequest()); + + Console.WriteLine(r2.Result.Match( + account => account.ToString(), + appwriteError => appwriteError.Message, + internalERror => internalERror.Message)); + + var r3 = await _client.Account.Get(); + + Console.WriteLine(r3.Result.Match( + account => account.ToString(), + appwriteError => appwriteError.Message, + internalERror => internalERror.Message)); } } diff --git a/src/PinguApps.Appwrite.Server/Internals/IBaseApi.cs b/src/PinguApps.Appwrite.Server/Internals/IBaseApi.cs index cffdc5ff..525309dd 100644 --- a/src/PinguApps.Appwrite.Server/Internals/IBaseApi.cs +++ b/src/PinguApps.Appwrite.Server/Internals/IBaseApi.cs @@ -7,7 +7,7 @@ namespace PinguApps.Appwrite.Server.Internals; "x-sdk-platform: server", "x-sdk-language: dotnet", "x-sdk-version: 0.0.1", - "X-Appwrite-Response-Format: 1.5.0")] + "X-Appwrite-Response-Format: 1.6.0")] internal interface IBaseApi { } diff --git a/src/PinguApps.Appwrite.Shared/Requests/DeleteSessionRequest.cs b/src/PinguApps.Appwrite.Shared/Requests/DeleteSessionRequest.cs new file mode 100644 index 00000000..1d5cbd29 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Requests/DeleteSessionRequest.cs @@ -0,0 +1,16 @@ +ο»Ώusing System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Requests.Validators; + +namespace PinguApps.Appwrite.Shared.Requests; + +/// +/// The request for deleting a session (logging out of current session) +/// +public class DeleteSessionRequest : BaseRequest +{ + /// + /// Session ID. Use the string 'current' to delete the current device session. + /// + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = "current"; +} diff --git a/src/PinguApps.Appwrite.Shared/Requests/Validators/DeleteSessionRequestValidator.cs b/src/PinguApps.Appwrite.Shared/Requests/Validators/DeleteSessionRequestValidator.cs new file mode 100644 index 00000000..46e137b1 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Requests/Validators/DeleteSessionRequestValidator.cs @@ -0,0 +1,10 @@ +ο»Ώusing FluentValidation; + +namespace PinguApps.Appwrite.Shared.Requests.Validators; +public class DeleteSessionRequestValidator : AbstractValidator +{ + public DeleteSessionRequestValidator() + { + RuleFor(x => x.SessionId).NotEmpty(); + } +} diff --git a/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.DeleteSession.cs b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.DeleteSession.cs new file mode 100644 index 00000000..397c2587 --- /dev/null +++ b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.DeleteSession.cs @@ -0,0 +1,109 @@ +using System.Net; +using PinguApps.Appwrite.Client.Clients; +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 DeleteSession_ShouldReturnSuccess_WhenApiCallSucceeds() + { + // Arrange + var request = new DeleteSessionRequest(); + + _mockHttp.Expect(HttpMethod.Delete, $"{Constants.Endpoint}/account/sessions/current") + .ExpectedHeaders(true) + .Respond(Constants.AppJson, Constants.MfaTypeResponse); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.DeleteSession(request); + + // Assert + Assert.True(result.Success); + } + + [Fact] + public async Task DeleteSession_ShouldHitDifferentEndpoint_WhenNewTypeIsUsed() + { + // Arrange + var sessionId = "mySessionId"; + var request = new DeleteSessionRequest() + { + SessionId = sessionId + }; + var requestUri = $"{Constants.Endpoint}/account/sessions/{sessionId}"; + var mockRequest = _mockHttp.Expect(HttpMethod.Delete, requestUri) + .ExpectedHeaders(true) + .Respond(Constants.AppJson, Constants.MfaTypeResponse); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.DeleteSession(request); + + // Assert + _mockHttp.VerifyNoOutstandingExpectation(); + var matches = _mockHttp.GetMatchCount(mockRequest); + Assert.Equal(1, matches); + } + + [Fact] + public async Task DeleteSession_ShouldReturnError_WhenSessionIsNull() + { + // Arrange + var request = new DeleteSessionRequest(); + + // Act + var result = await _appwriteClient.Account.DeleteSession(request); + + // Assert + Assert.True(result.IsError); + Assert.True(result.IsInternalError); + Assert.Equal(ISessionAware.SessionExceptionMessage, result.Result.AsT2.Message); + } + + [Fact] + public async Task DeleteSession_ShouldHandleException_WhenApiCallFails() + { + // Arrange + var request = new DeleteSessionRequest(); + + _mockHttp.Expect(HttpMethod.Delete, $"{Constants.Endpoint}/account/sessions/current") + .ExpectedHeaders(true) + .Respond(HttpStatusCode.BadRequest, Constants.AppJson, Constants.AppwriteError); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.DeleteSession(request); + + // Assert + Assert.True(result.IsError); + Assert.True(result.IsAppwriteError); + } + + [Fact] + public async Task DeleteSession_ShouldReturnErrorResponse_WhenExceptionOccurs() + { + // Arrange + var request = new DeleteSessionRequest(); + + _mockHttp.Expect(HttpMethod.Delete, $"{Constants.Endpoint}/account/sessions/current") + .ExpectedHeaders(true) + .Throw(new HttpRequestException("An error occurred")); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.DeleteSession(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/Constants.cs b/tests/PinguApps.Appwrite.Shared.Tests/Constants.cs index fa340b04..2618458b 100644 --- a/tests/PinguApps.Appwrite.Shared.Tests/Constants.cs +++ b/tests/PinguApps.Appwrite.Shared.Tests/Constants.cs @@ -18,7 +18,7 @@ public static class Constants public const string SdkVersion = "0.0.1"; - public const string AppwriteResponseFormat = "1.5.0"; + public const string AppwriteResponseFormat = "1.6.0"; public const string AppwriteError = """ { diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Requests/DeleteSessionRequestTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Requests/DeleteSessionRequestTests.cs new file mode 100644 index 00000000..e2ce4e62 --- /dev/null +++ b/tests/PinguApps.Appwrite.Shared.Tests/Requests/DeleteSessionRequestTests.cs @@ -0,0 +1,93 @@ +using FluentValidation; +using PinguApps.Appwrite.Shared.Requests; + +namespace PinguApps.Appwrite.Shared.Tests.Requests; +public class DeleteSessionRequestTests +{ + [Fact] + public void Constructor_InitializesWithExpectedValues() + { + // Arrange & Act + var request = new DeleteSessionRequest(); + + // Assert + Assert.Equal("current", request.SessionId); + } + + [Theory] + [InlineData("A string")] + public void Properties_CanBeSet(string sessionId) + { + // Arrange + var request = new DeleteSessionRequest(); + + // Act + request.SessionId = sessionId; + + // Assert + Assert.Equal(sessionId, request.SessionId); + } + + [Fact] + public void IsValid_WithValidInputs_ReturnsTrue() + { + // Arrange + var request = new DeleteSessionRequest + { + SessionId = "blahblah" + }; + + // Act + var isValid = request.IsValid(); + + // Assert + Assert.True(isValid); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void IsValid_WithInvalidInputs_ReturnsFalse(string? sessionId) + { + // Arrange + var request = new DeleteSessionRequest + { + SessionId = sessionId! + }; + + // Act + var isValid = request.IsValid(); + + // Assert + Assert.False(isValid); + } + + [Fact] + public void Validate_WithThrowOnFailuresTrue_ThrowsValidationExceptionOnFailure() + { + // Arrange + var request = new DeleteSessionRequest + { + SessionId = "" + }; + + // Assert + Assert.Throws(() => request.Validate(true)); + } + + [Fact] + public void Validate_WithThrowOnFailuresFalse_ReturnsInvalidResultOnFailure() + { + // Arrange + var request = new DeleteSessionRequest + { + SessionId = "" + }; + + // Act + var result = request.Validate(false); + + // Assert + Assert.False(result.IsValid); + } +}