From 8a30d996fbf4f5ca9be91a2619fff1f102071464 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Tue, 30 Jul 2024 00:35:05 +0200 Subject: [PATCH 1/4] Added functuonality for Update Session, including a workaround for 8447 in Appwrite repo --- .../Clients/AccountClient.cs | 15 +++++++ .../Clients/IAccountClient.cs | 8 ++++ .../Internals/IAccountApi.cs | 3 ++ src/PinguApps.Appwrite.Playground/App.cs | 23 ++++++++++- .../MultiFormatDateTimeConverter .cs | 40 +++++++++++++++++++ .../Responses/Session.cs | 2 +- 6 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 src/PinguApps.Appwrite.Shared/Converters/MultiFormatDateTimeConverter .cs diff --git a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs index 81d12b05..3cc3748a 100644 --- a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs @@ -214,4 +214,19 @@ public async Task> GetSession(string sessionId = "curren return e.GetExceptionResponse(); } } + + /// + public async Task> UpdateSession(string sessionId = "current") + { + try + { + var result = await _accountApi.UpdateSession(Session, 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 02fdafb9..b05c29aa 100644 --- a/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs @@ -100,4 +100,12 @@ public interface IAccountClient /// Session ID. Use the string 'current' to get the current device session /// The session Task> GetSession(string sessionId = "current"); + + /// + /// Use this endpoint to extend a session's length. Extending a session is useful when session expiry is short. If the session was created using an OAuth provider, this endpoint refreshes the access token from the provider. + /// Appwrite Docs + /// + /// Session ID. Use the string 'current' to update the current device session. + /// The session + Task> UpdateSession(string sessionId = "current"); } diff --git a/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs b/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs index 74a91453..bf8a9164 100644 --- a/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs +++ b/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs @@ -40,4 +40,7 @@ internal interface IAccountApi : IBaseApi [Get("/account/sessions/{sessionId}")] Task> GetSession([Header("x-appwrite-session")] string? session, string sessionId); + + [Patch("/account/sessions/{sessionId}")] + Task> UpdateSession([Header("x-appwrite-session")] string? session, string sessionId); } diff --git a/src/PinguApps.Appwrite.Playground/App.cs b/src/PinguApps.Appwrite.Playground/App.cs index 65ce91e2..6f70b111 100644 --- a/src/PinguApps.Appwrite.Playground/App.cs +++ b/src/PinguApps.Appwrite.Playground/App.cs @@ -22,9 +22,28 @@ public async Task Run(string[] args) Console.WriteLine("Getting Session..."); - var account = await _client.Account.GetSession("6695d717983fadf6ece1"); + //var response = await _client.Account.CreateEmailToken(new CreateEmailTokenRequest + //{ + // Email = "pingu@pinguapps.com", + // UserId = "664aac1a00113f82e620" + //}); - Console.WriteLine(account.Result.Match( + //var response = await _client.Account.CreateSession(new CreateSessionRequest + //{ + // UserId = "664aac1a00113f82e620", + // Secret = "623341" + //}); + + var response = await _client.Account.GetSession("66a810f2e55b1329e25b"); + + var response2 = await _client.Account.UpdateSession("66a810f2e55b1329e25b"); + + Console.WriteLine(response.Result.Match( + account => account.ToString(), + appwriteError => appwriteError.Message, + internalERror => internalERror.Message)); + + Console.WriteLine(response2.Result.Match( account => account.ToString(), appwriteError => appwriteError.Message, internalERror => internalERror.Message)); diff --git a/src/PinguApps.Appwrite.Shared/Converters/MultiFormatDateTimeConverter .cs b/src/PinguApps.Appwrite.Shared/Converters/MultiFormatDateTimeConverter .cs new file mode 100644 index 00000000..83f2cfca --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Converters/MultiFormatDateTimeConverter .cs @@ -0,0 +1,40 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace PinguApps.Appwrite.Shared.Converters; + +/// +/// This is only required temporarily as a workaround for #8447 on Appwrite. +/// +/// +public class MultiFormatDateTimeConverter : JsonConverter +{ + private readonly string[] _formats = [ + "yyyy-MM-ddTHH:mm:ss.fffK", + "yyyy-MM-dd HH:mm:ss.fff" + ]; + + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + var dateString = reader.GetString(); + + foreach (var format in _formats) + { + if (DateTime.TryParseExact(dateString, format, null, System.Globalization.DateTimeStyles.None, out var dateTime)) + { + return dateTime; + } + } + throw new JsonException($"Unable to parse date: {dateString}"); + } + throw new JsonException("Invalid token type"); + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString(_formats[0])); // Use the first format for serialization + } +} diff --git a/src/PinguApps.Appwrite.Shared/Responses/Session.cs b/src/PinguApps.Appwrite.Shared/Responses/Session.cs index 15f3f71f..d6375563 100644 --- a/src/PinguApps.Appwrite.Shared/Responses/Session.cs +++ b/src/PinguApps.Appwrite.Shared/Responses/Session.cs @@ -42,7 +42,7 @@ public record Session( [property: JsonPropertyName("$createdAt")] DateTime CreatedAt, [property: JsonPropertyName("$updatedAt")] DateTime UpdatedAt, [property: JsonPropertyName("userId")] string UserId, - [property: JsonPropertyName("expire")] DateTime ExpiresAt, + [property: JsonPropertyName("expire"), JsonConverter(typeof(MultiFormatDateTimeConverter))] DateTime ExpiresAt, [property: JsonPropertyName("provider")] string Provider, [property: JsonPropertyName("providerUid")] string ProviderUserId, [property: JsonPropertyName("providerAccessToken")] string ProviderAccessToken, From a37530d7cdb11867ef54f5b6fc589ba60d1bed00 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Thu, 1 Aug 2024 14:19:26 +0200 Subject: [PATCH 2/4] Added client tests --- .../AccountClientTests.UpdateSession.cs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.UpdateSession.cs diff --git a/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.UpdateSession.cs b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.UpdateSession.cs new file mode 100644 index 00000000..3b364b01 --- /dev/null +++ b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.UpdateSession.cs @@ -0,0 +1,81 @@ +using System.Net; +using PinguApps.Appwrite.Shared.Tests; +using PinguApps.Appwrite.Shared.Utils; +using RichardSzalay.MockHttp; + +namespace PinguApps.Appwrite.Client.Tests.Clients.Account; +public partial class AccountClientTests +{ + [Fact] + public async Task UpdateSession_HitsCurrent_ShouldReturnSuccess_WhenApiCallSucceeds() + { + // Arrange + _mockHttp.Expect(HttpMethod.Patch, $"{Constants.Endpoint}/account/sessions/current") + .ExpectedHeaders(true) + .Respond(Constants.AppJson, Constants.UserResponse); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.UpdateSession(); + + // Assert + Assert.True(result.Success); + } + + [Fact] + public async Task UpdateSession_ShouldReturnSuccess_WhenApiCallSucceeds() + { + // Arrange + var sessionId = IdUtils.GenerateUniqueId(); + + _mockHttp.Expect(HttpMethod.Patch, $"{Constants.Endpoint}/account/sessions/{sessionId}") + .ExpectedHeaders(true) + .Respond(Constants.AppJson, Constants.UserResponse); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.UpdateSession(sessionId); + + // Assert + Assert.True(result.Success); + } + + [Fact] + public async Task UpdateSession_ShouldHandleException_WhenApiCallFails() + { + // Arrange + _mockHttp.Expect(HttpMethod.Patch, $"{Constants.Endpoint}/account/sessions/current") + .ExpectedHeaders(true) + .Respond(HttpStatusCode.BadRequest, Constants.AppJson, Constants.AppwriteError); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.UpdateSession(); + + // Assert + Assert.True(result.IsError); + Assert.True(result.IsAppwriteError); + } + + [Fact] + public async Task UpdateSession_ShouldReturnErrorResponse_WhenExceptionOccurs() + { + // Arrange + _mockHttp.Expect(HttpMethod.Patch, $"{Constants.Endpoint}/account/sessions/current") + .ExpectedHeaders(true) + .Throw(new HttpRequestException("An error occurred")); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.UpdateSession(); + + // Assert + Assert.False(result.Success); + Assert.True(result.IsInternalError); + Assert.Equal("An error occurred", result.Result.AsT2.Message); + } +} From c5d62a9cdf382599010549ea791ff3d2f7c32085 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Thu, 1 Aug 2024 14:37:54 +0200 Subject: [PATCH 3/4] Added full test coverage for shared --- .../MultiFormatDateTimeConverterTests.cs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/PinguApps.Appwrite.Shared.Tests/Converters/MultiFormatDateTimeConverterTests.cs diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Converters/MultiFormatDateTimeConverterTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Converters/MultiFormatDateTimeConverterTests.cs new file mode 100644 index 00000000..382eb00e --- /dev/null +++ b/tests/PinguApps.Appwrite.Shared.Tests/Converters/MultiFormatDateTimeConverterTests.cs @@ -0,0 +1,88 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Converters; + +namespace PinguApps.Appwrite.Shared.Tests.Converters; +public class MultiFormatDateTimeConverterTests +{ + private readonly JsonSerializerOptions _options; + + public MultiFormatDateTimeConverterTests() + { + _options = new JsonSerializerOptions(); + _options.Converters.Add(new MultiFormatDateTimeConverter()); + } + + [Fact] + public void Read_ValidDateStringWithTimeZone_ReturnsDateTime() + { + var json = "\"2023-01-01T00:00:00.000Z\""; + var result = JsonSerializer.Deserialize(json, _options); + + // Convert both to UTC to compare + var expectedDateTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var actualDateTime = result.ToUniversalTime(); + + Assert.Equal(expectedDateTime, actualDateTime); + } + + [Fact] + public void Read_ValidDateStringWithoutTimeZone_ReturnsDateTime() + { + var json = "\"2023-01-01 00:00:00.000\""; + var result = JsonSerializer.Deserialize(json, _options); + Assert.Equal(new DateTime(2023, 1, 1, 0, 0, 0), result); + } + + [Fact] + public void Read_InvalidDateString_ThrowsJsonException() + { + var json = "\"invalid-date\""; + Assert.Throws(() => JsonSerializer.Deserialize(json, _options)); + } + + [Fact] + public void Read_UnexpectedTokenType_ThrowsJsonException() + { + var json = "123"; + Assert.Throws(() => JsonSerializer.Deserialize(json, _options)); + } + + [Fact] + public void Write_ValidDateTime_WritesExpectedString() + { + var dateTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var json = JsonSerializer.Serialize(dateTime, _options); + Assert.Equal("\"2023-01-01T00:00:00.000Z\"", json); + } + + public class MultiFormatDateTimeObject + { + [JsonPropertyName("x")] + [JsonConverter(typeof(MultiFormatDateTimeConverter))] + public DateTime X { get; set; } + } + + [Fact] + public void Read_ValidDateStringInObject_ReturnsDateTime() + { + var json = "{\"x\": \"2023-01-01T00:00:00.000Z\"}"; + var result = JsonSerializer.Deserialize(json, _options); + Assert.NotNull(result); + + + // Convert both to UTC to compare + var expectedDateTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var actualDateTime = result.X.ToUniversalTime(); + + Assert.Equal(expectedDateTime, actualDateTime); + } + + [Fact] + public void Write_ValidDateTimeInObject_WritesExpectedString() + { + var obj = new MultiFormatDateTimeObject { X = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc) }; + var json = JsonSerializer.Serialize(obj, _options); + Assert.Equal("{\"x\":\"2023-01-01T00:00:00.000Z\"}", json); + } +} From bf3a132c57c1d8bf46e291a3003d4b1c76731ec6 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Thu, 1 Aug 2024 23:48:16 +0200 Subject: [PATCH 4/4] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bd799bba..5188eef8 100644 --- a/README.md +++ b/README.md @@ -139,11 +139,11 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( ## ⌛ Progress ### Server & Client -![13 / 288](https://progress-bar.dev/13/?scale=288&suffix=%20/%20288&width=500) +![14 / 288](https://progress-bar.dev/14/?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 -![11 / 93](https://progress-bar.dev/11/?scale=93&suffix=%20/%2093&width=300) +![12 / 93](https://progress-bar.dev/12/?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 -![13 / 52](https://progress-bar.dev/13/?scale=52&suffix=%20/%2052&width=120) +![14 / 52](https://progress-bar.dev/14/?scale=52&suffix=%20/%2052&width=120) | Endpoint | Client | Server | |:-:|:-:|:-:| @@ -190,7 +190,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( | [Update Phone Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#updatePhoneSession) | ⬛ | ❌ | | [Create Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#createSession) | ✅ | ❌ | | [Get Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#getSession) | ✅ | ❌ | -| [Update Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateSession) | ⬛ | ❌ | +| [Update Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateSession) | ✅ | ❌ | | [Delete Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#deleteSession) | ⬛ | ❌ | | [Update Status](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateStatus) | ⬛ | ❌ | | [Create Push Target](https://appwrite.io/docs/references/1.5.x/client-rest/account#createPushTarget) | ⬛ | ❌ |