From e5c7f541c7d9fd107c89c87567fbf94118a803c9 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Sun, 14 Jul 2024 01:09:24 +0100 Subject: [PATCH 01/16] Added session response type --- .../Responses/Session.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/PinguApps.Appwrite.Shared/Responses/Session.cs diff --git a/src/PinguApps.Appwrite.Shared/Responses/Session.cs b/src/PinguApps.Appwrite.Shared/Responses/Session.cs new file mode 100644 index 00000000..9c14e8b4 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Responses/Session.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; + +namespace PinguApps.Appwrite.Shared.Responses; + +/// +/// An Appwrite Session object +/// +/// Session ID +/// Session creation date in ISO 8601 format +/// Session update date in ISO 8601 format +/// User ID +/// Session expiration date in ISO 8601 format +/// Session Provider +/// Session Provider User ID +/// Session Provider Access Token +/// The date of when the access token expires in ISO 8601 format +/// Session Provider Refresh Token +/// IP in use when the session was created +/// Operating system code name. View list of available options +/// Operating system name +/// Operating system version +/// Client type +/// Client code name. View list of available options +/// Client name +/// Client version +/// Client engine name +/// Client engine name +/// Device name +/// Device brand name +/// Device model name +/// Country two-character ISO 3166-1 alpha code +/// Country name +/// Returns true if this the current user session +/// Returns a list of active session factors +/// Secret used to authenticate the user. Only included if the request was made with an API key +/// Most recent date in ISO 8601 format when the session successfully passed MFA challenge +public record Session( + string Id, + DateTime CreatedAt, + DateTime UpdatedAt, + string UserId, + DateTime ExpiresAt, + string Provider, + string ProviderId, + string ProviderAccessToken, + DateTime? ProviderAccessTokenExpiry, + string ProviderRefreshToken, + string Ip, + string OsCode, + string OsName, + string OsVersion, + string ClientType, + string ClientCode, + string ClientName, + string ClientVersion, + string ClientEngine, + string ClientEngineVersion, + string DeviceName, + string DeviceBrand, + string DeviceModel, + string CountryCode, + string CountryName, + bool Current, + IReadOnlyList Factors, + string Secret, + DateTime? MfaUpdatedAt +); From 9707518f8587c2bade3d5479d9ce738331121fcc Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Sun, 14 Jul 2024 01:55:17 +0100 Subject: [PATCH 02/16] Added session tests --- .../Converters/NullableDateTimeConverter.cs | 45 +++++++++ .../Responses/Session.cs | 62 +++++++------ .../Constants.cs | 36 ++++++++ .../Responses/SessionTests.cs | 92 +++++++++++++++++++ 4 files changed, 205 insertions(+), 30 deletions(-) create mode 100644 src/PinguApps.Appwrite.Shared/Converters/NullableDateTimeConverter.cs create mode 100644 tests/PinguApps.Appwrite.Shared.Tests/Responses/SessionTests.cs diff --git a/src/PinguApps.Appwrite.Shared/Converters/NullableDateTimeConverter.cs b/src/PinguApps.Appwrite.Shared/Converters/NullableDateTimeConverter.cs new file mode 100644 index 00000000..eddda37f --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Converters/NullableDateTimeConverter.cs @@ -0,0 +1,45 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace PinguApps.Appwrite.Shared.Converters; +internal class NullableDateTimeConverter : JsonConverter +{ + public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + var stringValue = reader.GetString(); + if (string.IsNullOrEmpty(stringValue)) + { + return null; + } + + if (DateTime.TryParse(stringValue, out var dateTime)) + { + return dateTime; + } + + throw new JsonException($"Unable to parse '{stringValue}' to DateTime."); + } + + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + throw new JsonException("Unexpected token type."); + } + + public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options) + { + if (value.HasValue) + { + writer.WriteStringValue(value.Value.ToString("o")); + } + else + { + writer.WriteNullValue(); + } + } +} diff --git a/src/PinguApps.Appwrite.Shared/Responses/Session.cs b/src/PinguApps.Appwrite.Shared/Responses/Session.cs index 9c14e8b4..7f4f9b2c 100644 --- a/src/PinguApps.Appwrite.Shared/Responses/Session.cs +++ b/src/PinguApps.Appwrite.Shared/Responses/Session.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Converters; namespace PinguApps.Appwrite.Shared.Responses; @@ -12,7 +14,7 @@ namespace PinguApps.Appwrite.Shared.Responses; /// User ID /// Session expiration date in ISO 8601 format /// Session Provider -/// Session Provider User ID +/// Session Provider User ID /// Session Provider Access Token /// The date of when the access token expires in ISO 8601 format /// Session Provider Refresh Token @@ -36,33 +38,33 @@ namespace PinguApps.Appwrite.Shared.Responses; /// Secret used to authenticate the user. Only included if the request was made with an API key /// Most recent date in ISO 8601 format when the session successfully passed MFA challenge public record Session( - string Id, - DateTime CreatedAt, - DateTime UpdatedAt, - string UserId, - DateTime ExpiresAt, - string Provider, - string ProviderId, - string ProviderAccessToken, - DateTime? ProviderAccessTokenExpiry, - string ProviderRefreshToken, - string Ip, - string OsCode, - string OsName, - string OsVersion, - string ClientType, - string ClientCode, - string ClientName, - string ClientVersion, - string ClientEngine, - string ClientEngineVersion, - string DeviceName, - string DeviceBrand, - string DeviceModel, - string CountryCode, - string CountryName, - bool Current, - IReadOnlyList Factors, - string Secret, - DateTime? MfaUpdatedAt + [property: JsonPropertyName("$id")] string Id, + [property: JsonPropertyName("$createdAt")] DateTime CreatedAt, + [property: JsonPropertyName("$updatedAt")] DateTime UpdatedAt, + [property: JsonPropertyName("userId")] string UserId, + [property: JsonPropertyName("expire")] DateTime ExpiresAt, + [property: JsonPropertyName("provider")] string Provider, + [property: JsonPropertyName("providerUid")] string ProviderUserId, + [property: JsonPropertyName("providerAccessToken")] string ProviderAccessToken, + [property: JsonPropertyName("providerAccessTokenExpiry")] DateTime? ProviderAccessTokenExpiry, + [property: JsonPropertyName("providerRefreshToken")] string ProviderRefreshToken, + [property: JsonPropertyName("ip")] string Ip, + [property: JsonPropertyName("osCode")] string OsCode, + [property: JsonPropertyName("osName")] string OsName, + [property: JsonPropertyName("osVersion")] string OsVersion, + [property: JsonPropertyName("clientType")] string ClientType, + [property: JsonPropertyName("clientCode")] string ClientCode, + [property: JsonPropertyName("clientName")] string ClientName, + [property: JsonPropertyName("clientVersion")] string ClientVersion, + [property: JsonPropertyName("clientEngine")] string ClientEngine, + [property: JsonPropertyName("clientEngineVersion")] string ClientEngineVersion, + [property: JsonPropertyName("deviceName")] string DeviceName, + [property: JsonPropertyName("deviceBrand")] string DeviceBrand, + [property: JsonPropertyName("deviceModel")] string DeviceModel, + [property: JsonPropertyName("countryCode")] string CountryCode, + [property: JsonPropertyName("countryName")] string CountryName, + [property: JsonPropertyName("current")] bool Current, + [property: JsonPropertyName("factors")] IReadOnlyList Factors, + [property: JsonPropertyName("secret")] string Secret, + [property: JsonPropertyName("mfaUpdatedAt"), JsonConverter(typeof(NullableDateTimeConverter))] DateTime? MfaUpdatedAt ); diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Constants.cs b/tests/PinguApps.Appwrite.Shared.Tests/Constants.cs index 2374dec7..aefef2fb 100644 --- a/tests/PinguApps.Appwrite.Shared.Tests/Constants.cs +++ b/tests/PinguApps.Appwrite.Shared.Tests/Constants.cs @@ -90,4 +90,40 @@ public static class Constants "phrase": "Golden Fox" } """; + + public const string SessionResponse = """ + { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "userId": "5e5bb8c16897e", + "expire": "2020-10-15T06:38:00.000+00:00", + "provider": "email", + "providerUid": "user@example.com", + "providerAccessToken": "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", + "providerAccessTokenExpiry": "2020-10-15T06:38:00.000+00:00", + "providerRefreshToken": "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", + "ip": "127.0.0.1", + "osCode": "Mac", + "osName": "Mac", + "osVersion": "Mac", + "clientType": "browser", + "clientCode": "CM", + "clientName": "Chrome Mobile iOS", + "clientVersion": "84.0", + "clientEngine": "WebKit", + "clientEngineVersion": "605.1.15", + "deviceName": "smartphone", + "deviceBrand": "Google", + "deviceModel": "Nexus 5", + "countryCode": "US", + "countryName": "United States", + "current": true, + "factors": [ + "email" + ], + "secret": "5e5bb8c16897e", + "mfaUpdatedAt": "2020-10-15T06:38:00.000+00:00" + } + """; } diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Responses/SessionTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Responses/SessionTests.cs new file mode 100644 index 00000000..6f1fd979 --- /dev/null +++ b/tests/PinguApps.Appwrite.Shared.Tests/Responses/SessionTests.cs @@ -0,0 +1,92 @@ +using System.Text.Json; +using PinguApps.Appwrite.Shared.Responses; + +namespace PinguApps.Appwrite.Shared.Tests.Responses; +public class SessionTests +{ + [Fact] + public void Session_Constructor_AssignsPropertiesCorrectly() + { + // Arrange + var id = "5e5ea5c16897e"; + var createdAt = DateTime.UtcNow; + var updatedAt = DateTime.UtcNow; + var userId = "5e5bb8c16897e"; + var expiresAt = DateTime.UtcNow; + var provider = "email"; + var providerUserId = "user@example.com"; + var providerAccessToken = "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"; + var providerAccessTokenExpiry = DateTime.UtcNow; + var ProviderRefreshToken = "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"; + var ip = "127.0.0.1"; + var osCode = "Mac"; + var osName = "Mac"; + var osVersion = "Mac"; + var clientType = "browser"; + var clientCode = "CM"; + var clientName = "Chrome Mobile iOS"; + var clientVersion = "84.0"; + var clientEngine = "WebKit"; + var clientEngineVersion = "605.1.15"; + var deviceName = "smartphone"; + var deviceBrand = "Google"; + var deviceModel = "Nexus 5"; + var countryCode = "US"; + var countryName = "United States"; + var current = true; + var factors = new List { "email" }; + var secret = "5e5bb8c16897e"; + var mfaUpdatedAt = DateTime.UtcNow; + + // Act + var session = new Session(id, createdAt, updatedAt, userId, expiresAt, provider, providerUserId, providerAccessToken, + providerAccessTokenExpiry, ProviderRefreshToken, ip, osCode, osName, osVersion, clientType, clientCode, clientName, + clientVersion, clientEngine, clientEngineVersion, deviceName, deviceBrand, deviceModel, countryCode, countryName, + current, factors, secret, mfaUpdatedAt); + + // Assert + Assert.Equal(id, session.Id); + } + + [Fact] + public void Session_CanBeDeserialized_FromJson() + { + // Act + var session = JsonSerializer.Deserialize(Constants.SessionResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + // Assert + Assert.NotNull(session); + Assert.Equal("5e5ea5c16897e", session.Id); + Assert.Equal(DateTime.Parse("2020-10-15T06:38:00.000+00:00").ToUniversalTime(), session.CreatedAt.ToUniversalTime()); + Assert.Equal(DateTime.Parse("2020-10-15T06:38:00.000+00:00").ToUniversalTime(), session.UpdatedAt.ToUniversalTime()); + Assert.Equal("5e5bb8c16897e", session.UserId); + Assert.Equal(DateTime.Parse("2020-10-15T06:38:00.000+00:00").ToUniversalTime(), session.ExpiresAt.ToUniversalTime()); + Assert.Equal("email", session.Provider); + Assert.Equal("user@example.com", session.ProviderUserId); + Assert.Equal("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", session.ProviderAccessToken); + Assert.NotNull(session.ProviderAccessTokenExpiry); + Assert.Equal(DateTime.Parse("2020-10-15T06:38:00.000+00:00").ToUniversalTime(), session.ProviderAccessTokenExpiry.Value.ToUniversalTime()); + Assert.Equal("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", session.ProviderRefreshToken); + Assert.Equal("127.0.0.1", session.Ip); + Assert.Equal("Mac", session.OsCode); + Assert.Equal("Mac", session.OsName); + Assert.Equal("Mac", session.OsVersion); + Assert.Equal("browser", session.ClientType); + Assert.Equal("CM", session.ClientCode); + Assert.Equal("Chrome Mobile iOS", session.ClientName); + Assert.Equal("84.0", session.ClientVersion); + Assert.Equal("WebKit", session.ClientEngine); + Assert.Equal("605.1.15", session.ClientEngineVersion); + Assert.Equal("smartphone", session.DeviceName); + Assert.Equal("Google", session.DeviceBrand); + Assert.Equal("Nexus 5", session.DeviceModel); + Assert.Equal("US", session.CountryCode); + Assert.Equal("United States", session.CountryName); + Assert.True(session.Current); + Assert.Single(session.Factors); + Assert.Equal("email", session.Factors[0]); + Assert.Equal("5e5bb8c16897e", session.Secret); + Assert.NotNull(session.MfaUpdatedAt); + Assert.Equal(DateTime.Parse("2020-10-15T06:38:00.000+00:00").ToUniversalTime(), session.MfaUpdatedAt.Value.ToUniversalTime()); + } +} From 12aa3d0edfb073301648e292547aad73aff8c686 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Sun, 14 Jul 2024 01:55:57 +0100 Subject: [PATCH 03/16] Ensured both nullable datetimes are converted correctly --- src/PinguApps.Appwrite.Shared/Responses/Session.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PinguApps.Appwrite.Shared/Responses/Session.cs b/src/PinguApps.Appwrite.Shared/Responses/Session.cs index 7f4f9b2c..15f3f71f 100644 --- a/src/PinguApps.Appwrite.Shared/Responses/Session.cs +++ b/src/PinguApps.Appwrite.Shared/Responses/Session.cs @@ -46,7 +46,7 @@ public record Session( [property: JsonPropertyName("provider")] string Provider, [property: JsonPropertyName("providerUid")] string ProviderUserId, [property: JsonPropertyName("providerAccessToken")] string ProviderAccessToken, - [property: JsonPropertyName("providerAccessTokenExpiry")] DateTime? ProviderAccessTokenExpiry, + [property: JsonPropertyName("providerAccessTokenExpiry"), JsonConverter(typeof(NullableDateTimeConverter))] DateTime? ProviderAccessTokenExpiry, [property: JsonPropertyName("providerRefreshToken")] string ProviderRefreshToken, [property: JsonPropertyName("ip")] string Ip, [property: JsonPropertyName("osCode")] string OsCode, From f8b2a84c8394a5201c08cd3c8aae40c0db24e023 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Sun, 14 Jul 2024 03:22:56 +0100 Subject: [PATCH 04/16] Added session updating for Client Create Session --- .../Clients/AccountClient.cs | 54 +++++++++++++++++++ .../Clients/AppwriteClient.cs | 14 ++++- .../Clients/IAccountClient.cs | 9 ++++ .../Clients/ISessionAware.cs | 6 ++- .../Internals/CookieSessionData.cs | 11 ++++ .../Internals/IAccountApi.cs | 3 ++ src/PinguApps.Appwrite.Playground/App.cs | 11 ++-- src/PinguApps.Appwrite.Playground/Program.cs | 4 +- .../Requests/CreateSessionRequest.cs | 23 ++++++++ .../CreateSessionRequestValidator.cs | 11 ++++ 10 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 src/PinguApps.Appwrite.Client/Internals/CookieSessionData.cs create mode 100644 src/PinguApps.Appwrite.Shared/Requests/CreateSessionRequest.cs create mode 100644 src/PinguApps.Appwrite.Shared/Requests/Validators/CreateSessionRequestValidator.cs diff --git a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs index 1b5851d4..6067f4e7 100644 --- a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using PinguApps.Appwrite.Client.Clients; @@ -8,6 +11,7 @@ using PinguApps.Appwrite.Shared; using PinguApps.Appwrite.Shared.Requests; using PinguApps.Appwrite.Shared.Responses; +using Refit; namespace PinguApps.Appwrite.Client; @@ -22,6 +26,8 @@ public AccountClient(IServiceProvider services) string? ISessionAware.Session { get; set; } + public event EventHandler? SessionChanged; + ISessionAware? _sessionAware; public string? Session => GetSession(); private string? GetSession() @@ -34,6 +40,32 @@ public AccountClient(IServiceProvider services) return _sessionAware.Session; } + private void SaveSession(IApiResponse response) + { + if (response.IsSuccessStatusCode && SessionChanged is not null) + { + if (response.Headers.TryGetValues("Set-Cookie", out var values)) + { + var sessionCookie = values.FirstOrDefault(x => x.StartsWith("a_session", StringComparison.OrdinalIgnoreCase) && !x.Contains("legacy", StringComparison.OrdinalIgnoreCase)); + + if (sessionCookie is null) + return; + + var base64 = sessionCookie.Split('=')[1].Split(';')[0]; + + var decodedBytes = Convert.FromBase64String(base64); + var decoded = Encoding.UTF8.GetString(decodedBytes); + + var sessionData = JsonSerializer.Deserialize(decoded); + + if (sessionData is null) + return; + + SessionChanged.Invoke(this, sessionData.Secret); + } + } + } + /// public async Task> Get() { @@ -182,4 +214,26 @@ public async Task> CreateEmailToken(CreateEmailTokenReques return e.GetExceptionResponse(); } } + + /// + public async Task> CreateSession(CreateSessionRequest request, bool saveSession = true) + { + try + { + request.Validate(true); + + var result = await _accountApi.CreateSession(request); + + if (saveSession) + { + SaveSession(result); + } + + return result.GetApiResponse(); + } + catch (Exception e) + { + return e.GetExceptionResponse(); + } + } } diff --git a/src/PinguApps.Appwrite.Client/Clients/AppwriteClient.cs b/src/PinguApps.Appwrite.Client/Clients/AppwriteClient.cs index 53f727c3..04679c1a 100644 --- a/src/PinguApps.Appwrite.Client/Clients/AppwriteClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/AppwriteClient.cs @@ -1,4 +1,5 @@ -using PinguApps.Appwrite.Client.Clients; +using System; +using PinguApps.Appwrite.Client.Clients; namespace PinguApps.Appwrite.Client; public class AppwriteClient : IAppwriteClient, ISessionAware @@ -9,10 +10,16 @@ public class AppwriteClient : IAppwriteClient, ISessionAware public AppwriteClient(IAccountClient accountClient) { Account = accountClient; + if (Account is ISessionAware sessionAwareAccount) + { + sessionAwareAccount.SessionChanged += OnSessionChanged; + } } string? ISessionAware.Session { get; set; } + public event EventHandler? SessionChanged; + ISessionAware? _sessionAware; /// public string? Session => GetSession(); @@ -32,4 +39,9 @@ public void SetSession(string? session) (this as ISessionAware).UpdateSession(session); (Account as ISessionAware)!.UpdateSession(session); } + + private void OnSessionChanged(object? sender, string? newSession) + { + SetSession(newSession); + } } diff --git a/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs b/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs index 40fc460b..2772f547 100644 --- a/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs @@ -84,4 +84,13 @@ public interface IAccountClient /// The request content /// The token Task> CreateEmailToken(CreateEmailTokenRequest request); + + /// + /// Use this endpoint to create a session from token. Provide the userId and secret parameters from the successful response of authentication flows initiated by token creation. For example, magic URL and phone login. + /// Appwrite Docs + /// + /// The request content + /// If true, on successful response will set the client's session + /// The session + Task> CreateSession(CreateSessionRequest request, bool saveSession = true); } diff --git a/src/PinguApps.Appwrite.Client/Clients/ISessionAware.cs b/src/PinguApps.Appwrite.Client/Clients/ISessionAware.cs index 60672b1d..c1131555 100644 --- a/src/PinguApps.Appwrite.Client/Clients/ISessionAware.cs +++ b/src/PinguApps.Appwrite.Client/Clients/ISessionAware.cs @@ -1,8 +1,12 @@ -namespace PinguApps.Appwrite.Client.Clients; +using System; + +namespace PinguApps.Appwrite.Client.Clients; internal interface ISessionAware { public string? Session { get; protected set; } public void UpdateSession(string? session) => Session = session; + + event EventHandler? SessionChanged; } diff --git a/src/PinguApps.Appwrite.Client/Internals/CookieSessionData.cs b/src/PinguApps.Appwrite.Client/Internals/CookieSessionData.cs new file mode 100644 index 00000000..ee251ca1 --- /dev/null +++ b/src/PinguApps.Appwrite.Client/Internals/CookieSessionData.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace PinguApps.Appwrite.Client.Internals; +internal class CookieSessionData +{ + [JsonPropertyName("id")] + public string Id { get; set; } = default!; + + [JsonPropertyName("secret")] + public string Secret { get; set; } = default!; +} diff --git a/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs b/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs index f978dfdb..6fb39098 100644 --- a/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs +++ b/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs @@ -34,4 +34,7 @@ internal interface IAccountApi : IBaseApi [Post("/account/tokens/email")] Task> CreateEmailToken(CreateEmailTokenRequest request); + + [Post("/account/sessions/token")] + Task> CreateSession(CreateSessionRequest request); } diff --git a/src/PinguApps.Appwrite.Playground/App.cs b/src/PinguApps.Appwrite.Playground/App.cs index 49aa8b6c..1a790048 100644 --- a/src/PinguApps.Appwrite.Playground/App.cs +++ b/src/PinguApps.Appwrite.Playground/App.cs @@ -21,14 +21,17 @@ public async Task Run(string[] args) { //_client.SetSession(_session); - var request = new CreateEmailTokenRequest + var request = new CreateSessionRequest { - Email = "pingu@example.com", UserId = "664aac1a00113f82e620", - Phrase = true + Secret = "287856" }; - var result = await _server.Account.CreateEmailToken(request); + Console.WriteLine($"Session: {_client.Session}"); + + var result = await _client.Account.CreateSession(request, true); + + Console.WriteLine($"Session: {_client.Session}"); result.Result.Switch( account => Console.WriteLine(string.Join(',', account)), diff --git a/src/PinguApps.Appwrite.Playground/Program.cs b/src/PinguApps.Appwrite.Playground/Program.cs index 07824411..2f280220 100644 --- a/src/PinguApps.Appwrite.Playground/Program.cs +++ b/src/PinguApps.Appwrite.Playground/Program.cs @@ -7,8 +7,8 @@ var builder = Host.CreateApplicationBuilder(args); -//builder.Services.AddAppwriteClient(builder.Configuration.GetValue("ProjectId")!); -builder.Services.AddAppwriteClientForServer(builder.Configuration.GetValue("ProjectId")!); +builder.Services.AddAppwriteClient(builder.Configuration.GetValue("ProjectId")!); +//builder.Services.AddAppwriteClientForServer(builder.Configuration.GetValue("ProjectId")!); builder.Services.AddAppwriteServer(builder.Configuration.GetValue("ProjectId")!, builder.Configuration.GetValue("ApiKey")!); builder.Services.AddSingleton(); diff --git a/src/PinguApps.Appwrite.Shared/Requests/CreateSessionRequest.cs b/src/PinguApps.Appwrite.Shared/Requests/CreateSessionRequest.cs new file mode 100644 index 00000000..cd956f78 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Requests/CreateSessionRequest.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Requests.Validators; +using PinguApps.Appwrite.Shared.Utils; + +namespace PinguApps.Appwrite.Shared.Requests; + +/// +/// The request for creating a session +/// +public class CreateSessionRequest : BaseRequest +{ + /// + /// User ID. Choose a custom ID or generate a random ID with . Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars + /// + [JsonPropertyName("userId")] + public string UserId { get; set; } = IdUtils.GenerateUniqueId(); + + /// + /// Secret of a token generated by login methods. For example, the CreateMagicURLToken or CreatePhoneToken methods. + /// + [JsonPropertyName("secret")] + public string Secret { get; set; } = string.Empty; +} diff --git a/src/PinguApps.Appwrite.Shared/Requests/Validators/CreateSessionRequestValidator.cs b/src/PinguApps.Appwrite.Shared/Requests/Validators/CreateSessionRequestValidator.cs new file mode 100644 index 00000000..75dbcbe8 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Requests/Validators/CreateSessionRequestValidator.cs @@ -0,0 +1,11 @@ +using FluentValidation; + +namespace PinguApps.Appwrite.Shared.Requests.Validators; +public class CreateSessionRequestValidator : AbstractValidator +{ + public CreateSessionRequestValidator() + { + RuleFor(x => x.UserId).NotEmpty().Matches("^[a-zA-Z0-9][a-zA-Z0-9._-]{0,35}$"); + RuleFor(x => x.Secret).NotEmpty(); + } +} From 49e57bf7706748ad8f05de9d93659f467b4e4a8c Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Sun, 14 Jul 2024 03:42:27 +0100 Subject: [PATCH 05/16] Changed DI so that we only save session when using on client side --- .../Clients/AccountClient.cs | 13 ++++++------- .../Clients/IAccountClient.cs | 3 +-- .../ServiceCollectionExtensions.cs | 4 ++-- src/PinguApps.Appwrite.Playground/App.cs | 13 +++++++++++-- src/PinguApps.Appwrite.Playground/Program.cs | 4 ++-- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs index 6067f4e7..0bf992a3 100644 --- a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs @@ -18,10 +18,12 @@ namespace PinguApps.Appwrite.Client; public class AccountClient : IAccountClient, ISessionAware { private readonly IAccountApi _accountApi; + private readonly bool _saveSession; - public AccountClient(IServiceProvider services) + public AccountClient(IServiceProvider services, bool saveSession) { _accountApi = services.GetRequiredService(); + _saveSession = saveSession; } string? ISessionAware.Session { get; set; } @@ -42,7 +44,7 @@ public AccountClient(IServiceProvider services) private void SaveSession(IApiResponse response) { - if (response.IsSuccessStatusCode && SessionChanged is not null) + if (_saveSession && response.IsSuccessStatusCode && SessionChanged is not null) { if (response.Headers.TryGetValues("Set-Cookie", out var values)) { @@ -216,7 +218,7 @@ public async Task> CreateEmailToken(CreateEmailTokenReques } /// - public async Task> CreateSession(CreateSessionRequest request, bool saveSession = true) + public async Task> CreateSession(CreateSessionRequest request) { try { @@ -224,10 +226,7 @@ public async Task> CreateSession(CreateSessionRequest re var result = await _accountApi.CreateSession(request); - if (saveSession) - { - SaveSession(result); - } + SaveSession(result); return result.GetApiResponse(); } diff --git a/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs b/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs index 2772f547..b2f63233 100644 --- a/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs @@ -90,7 +90,6 @@ public interface IAccountClient /// Appwrite Docs /// /// The request content - /// If true, on successful response will set the client's session /// The session - Task> CreateSession(CreateSessionRequest request, bool saveSession = true); + Task> CreateSession(CreateSessionRequest request); } diff --git a/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs b/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs index 339b00bd..a64cd46f 100644 --- a/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs +++ b/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs @@ -27,7 +27,7 @@ public static IServiceCollection AddAppwriteClient(this IServiceCollection servi .ConfigureHttpClient(x => x.BaseAddress = new Uri(endpoint)) .AddHttpMessageHandler(); - services.AddSingleton(); + services.AddSingleton(x => new AccountClient(x, true)); services.AddSingleton(); return services; @@ -49,7 +49,7 @@ public static IServiceCollection AddAppwriteClientForServer(this IServiceCollect .ConfigureHttpClient(x => x.BaseAddress = new Uri(endpoint)) .AddHttpMessageHandler(); - services.AddTransient(); + services.AddTransient(x => new AccountClient(x, false)); services.AddTransient(); return services; diff --git a/src/PinguApps.Appwrite.Playground/App.cs b/src/PinguApps.Appwrite.Playground/App.cs index 1a790048..0372a4b2 100644 --- a/src/PinguApps.Appwrite.Playground/App.cs +++ b/src/PinguApps.Appwrite.Playground/App.cs @@ -24,12 +24,12 @@ public async Task Run(string[] args) var request = new CreateSessionRequest { UserId = "664aac1a00113f82e620", - Secret = "287856" + Secret = "357871" }; Console.WriteLine($"Session: {_client.Session}"); - var result = await _client.Account.CreateSession(request, true); + var result = await _client.Account.CreateSession(request); Console.WriteLine($"Session: {_client.Session}"); @@ -38,5 +38,14 @@ public async Task Run(string[] args) appwriteError => Console.WriteLine(appwriteError.Message), internalError => Console.WriteLine(internalError.Message) ); + + Console.WriteLine("Getting Account..."); + + var account = await _client.Account.Get(); + + Console.WriteLine(account.Result.Match( + account => account.ToString(), + appwriteError => appwriteError.Message, + internalERror => internalERror.Message)); } } diff --git a/src/PinguApps.Appwrite.Playground/Program.cs b/src/PinguApps.Appwrite.Playground/Program.cs index 2f280220..07824411 100644 --- a/src/PinguApps.Appwrite.Playground/Program.cs +++ b/src/PinguApps.Appwrite.Playground/Program.cs @@ -7,8 +7,8 @@ var builder = Host.CreateApplicationBuilder(args); -builder.Services.AddAppwriteClient(builder.Configuration.GetValue("ProjectId")!); -//builder.Services.AddAppwriteClientForServer(builder.Configuration.GetValue("ProjectId")!); +//builder.Services.AddAppwriteClient(builder.Configuration.GetValue("ProjectId")!); +builder.Services.AddAppwriteClientForServer(builder.Configuration.GetValue("ProjectId")!); builder.Services.AddAppwriteServer(builder.Configuration.GetValue("ProjectId")!, builder.Configuration.GetValue("ApiKey")!); builder.Services.AddSingleton(); From 009cc045cdb54c9df63727d879b69df00ae6e84e Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Sun, 14 Jul 2024 04:33:32 +0100 Subject: [PATCH 06/16] Found a way to not use cookies with server --- .../ServiceCollectionExtensions.cs | 25 ++++++++++++++++--- src/PinguApps.Appwrite.Playground/App.cs | 2 +- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs b/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs index a64cd46f..0f178000 100644 --- a/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs +++ b/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Net.Http; using Microsoft.Extensions.DependencyInjection; using PinguApps.Appwrite.Client.Handlers; using PinguApps.Appwrite.Client.Internals; @@ -45,9 +46,27 @@ public static IServiceCollection AddAppwriteClientForServer(this IServiceCollect { services.AddSingleton(sp => new HeaderHandler(projectId)); - services.AddRefitClient(refitSettings) - .ConfigureHttpClient(x => x.BaseAddress = new Uri(endpoint)) - .AddHttpMessageHandler(); + services.AddTransient(x => + { + var primaryHandler = new HttpClientHandler() + { + UseCookies = false + }; + + var headerHandler = x.GetRequiredService(); + headerHandler.InnerHandler = primaryHandler; + + var client = new HttpClient(headerHandler) + { + BaseAddress = new Uri(endpoint) + }; + + return RestService.For(client, refitSettings); + }); + + //services.AddRefitClient(refitSettings) + // .ConfigureHttpClient(x => x.BaseAddress = new Uri(endpoint)) + // .AddHttpMessageHandler(); services.AddTransient(x => new AccountClient(x, false)); services.AddTransient(); diff --git a/src/PinguApps.Appwrite.Playground/App.cs b/src/PinguApps.Appwrite.Playground/App.cs index 0372a4b2..139affca 100644 --- a/src/PinguApps.Appwrite.Playground/App.cs +++ b/src/PinguApps.Appwrite.Playground/App.cs @@ -24,7 +24,7 @@ public async Task Run(string[] args) var request = new CreateSessionRequest { UserId = "664aac1a00113f82e620", - Secret = "357871" + Secret = "524366" }; Console.WriteLine($"Session: {_client.Session}"); From 33b5fe1b5e16fa35dc08e26b781fa6ff9424565e Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Sun, 14 Jul 2024 04:44:57 +0100 Subject: [PATCH 07/16] Optimised service collection extensions, we are simply not remembering cookies any longer --- .../ServiceCollectionExtensions.cs | 30 +++++-------------- src/PinguApps.Appwrite.Playground/App.cs | 2 +- src/PinguApps.Appwrite.Playground/Program.cs | 4 +-- 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs b/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs index 0f178000..d0af20cf 100644 --- a/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs +++ b/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs @@ -35,7 +35,7 @@ public static IServiceCollection AddAppwriteClient(this IServiceCollection servi } /// - /// Adds all necessary components for the Client SDK in a transient state. Best used on server-side to perform client SDK abilities on behalf of users + /// Adds all necessary components for the Client SDK such that session will not be remembered. Best used on server-side to perform client SDK abilities on behalf of users /// /// The service collection to add to /// Your Appwrite Project ID @@ -46,30 +46,16 @@ public static IServiceCollection AddAppwriteClientForServer(this IServiceCollect { services.AddSingleton(sp => new HeaderHandler(projectId)); - services.AddTransient(x => - { - var primaryHandler = new HttpClientHandler() + services.AddRefitClient(refitSettings) + .ConfigureHttpClient(x => x.BaseAddress = new Uri(endpoint)) + .AddHttpMessageHandler() + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { UseCookies = false - }; - - var headerHandler = x.GetRequiredService(); - headerHandler.InnerHandler = primaryHandler; - - var client = new HttpClient(headerHandler) - { - BaseAddress = new Uri(endpoint) - }; - - return RestService.For(client, refitSettings); - }); + }); - //services.AddRefitClient(refitSettings) - // .ConfigureHttpClient(x => x.BaseAddress = new Uri(endpoint)) - // .AddHttpMessageHandler(); - - services.AddTransient(x => new AccountClient(x, false)); - services.AddTransient(); + services.AddSingleton(x => new AccountClient(x, false)); + services.AddSingleton(); return services; } diff --git a/src/PinguApps.Appwrite.Playground/App.cs b/src/PinguApps.Appwrite.Playground/App.cs index 139affca..7c148f93 100644 --- a/src/PinguApps.Appwrite.Playground/App.cs +++ b/src/PinguApps.Appwrite.Playground/App.cs @@ -24,7 +24,7 @@ public async Task Run(string[] args) var request = new CreateSessionRequest { UserId = "664aac1a00113f82e620", - Secret = "524366" + Secret = "814123" }; Console.WriteLine($"Session: {_client.Session}"); diff --git a/src/PinguApps.Appwrite.Playground/Program.cs b/src/PinguApps.Appwrite.Playground/Program.cs index 07824411..2f280220 100644 --- a/src/PinguApps.Appwrite.Playground/Program.cs +++ b/src/PinguApps.Appwrite.Playground/Program.cs @@ -7,8 +7,8 @@ var builder = Host.CreateApplicationBuilder(args); -//builder.Services.AddAppwriteClient(builder.Configuration.GetValue("ProjectId")!); -builder.Services.AddAppwriteClientForServer(builder.Configuration.GetValue("ProjectId")!); +builder.Services.AddAppwriteClient(builder.Configuration.GetValue("ProjectId")!); +//builder.Services.AddAppwriteClientForServer(builder.Configuration.GetValue("ProjectId")!); builder.Services.AddAppwriteServer(builder.Configuration.GetValue("ProjectId")!, builder.Configuration.GetValue("ApiKey")!); builder.Services.AddSingleton(); From f6f54eb46c4bddae36b056ef96074eb2d321dbd7 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Sun, 14 Jul 2024 21:08:32 +0100 Subject: [PATCH 08/16] improved session cookie handling, abstracting more away to specific handlers --- .../Clients/AccountClient.cs | 38 +------------ .../Clients/AppwriteClient.cs | 14 +---- .../Clients/ISessionAware.cs | 6 +- .../Handlers/ClientCookieSessionHandler.cs | 56 +++++++++++++++++++ .../ServiceCollectionExtensions.cs | 11 ++-- src/PinguApps.Appwrite.Playground/App.cs | 2 +- 6 files changed, 67 insertions(+), 60 deletions(-) create mode 100644 src/PinguApps.Appwrite.Client/Handlers/ClientCookieSessionHandler.cs diff --git a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs index 0bf992a3..50010aa1 100644 --- a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using PinguApps.Appwrite.Client.Clients; @@ -11,25 +8,20 @@ using PinguApps.Appwrite.Shared; using PinguApps.Appwrite.Shared.Requests; using PinguApps.Appwrite.Shared.Responses; -using Refit; namespace PinguApps.Appwrite.Client; public class AccountClient : IAccountClient, ISessionAware { private readonly IAccountApi _accountApi; - private readonly bool _saveSession; - public AccountClient(IServiceProvider services, bool saveSession) + public AccountClient(IServiceProvider services) { _accountApi = services.GetRequiredService(); - _saveSession = saveSession; } string? ISessionAware.Session { get; set; } - public event EventHandler? SessionChanged; - ISessionAware? _sessionAware; public string? Session => GetSession(); private string? GetSession() @@ -42,32 +34,6 @@ public AccountClient(IServiceProvider services, bool saveSession) return _sessionAware.Session; } - private void SaveSession(IApiResponse response) - { - if (_saveSession && response.IsSuccessStatusCode && SessionChanged is not null) - { - if (response.Headers.TryGetValues("Set-Cookie", out var values)) - { - var sessionCookie = values.FirstOrDefault(x => x.StartsWith("a_session", StringComparison.OrdinalIgnoreCase) && !x.Contains("legacy", StringComparison.OrdinalIgnoreCase)); - - if (sessionCookie is null) - return; - - var base64 = sessionCookie.Split('=')[1].Split(';')[0]; - - var decodedBytes = Convert.FromBase64String(base64); - var decoded = Encoding.UTF8.GetString(decodedBytes); - - var sessionData = JsonSerializer.Deserialize(decoded); - - if (sessionData is null) - return; - - SessionChanged.Invoke(this, sessionData.Secret); - } - } - } - /// public async Task> Get() { @@ -226,8 +192,6 @@ public async Task> CreateSession(CreateSessionRequest re var result = await _accountApi.CreateSession(request); - SaveSession(result); - return result.GetApiResponse(); } catch (Exception e) diff --git a/src/PinguApps.Appwrite.Client/Clients/AppwriteClient.cs b/src/PinguApps.Appwrite.Client/Clients/AppwriteClient.cs index 04679c1a..53f727c3 100644 --- a/src/PinguApps.Appwrite.Client/Clients/AppwriteClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/AppwriteClient.cs @@ -1,5 +1,4 @@ -using System; -using PinguApps.Appwrite.Client.Clients; +using PinguApps.Appwrite.Client.Clients; namespace PinguApps.Appwrite.Client; public class AppwriteClient : IAppwriteClient, ISessionAware @@ -10,16 +9,10 @@ public class AppwriteClient : IAppwriteClient, ISessionAware public AppwriteClient(IAccountClient accountClient) { Account = accountClient; - if (Account is ISessionAware sessionAwareAccount) - { - sessionAwareAccount.SessionChanged += OnSessionChanged; - } } string? ISessionAware.Session { get; set; } - public event EventHandler? SessionChanged; - ISessionAware? _sessionAware; /// public string? Session => GetSession(); @@ -39,9 +32,4 @@ public void SetSession(string? session) (this as ISessionAware).UpdateSession(session); (Account as ISessionAware)!.UpdateSession(session); } - - private void OnSessionChanged(object? sender, string? newSession) - { - SetSession(newSession); - } } diff --git a/src/PinguApps.Appwrite.Client/Clients/ISessionAware.cs b/src/PinguApps.Appwrite.Client/Clients/ISessionAware.cs index c1131555..60672b1d 100644 --- a/src/PinguApps.Appwrite.Client/Clients/ISessionAware.cs +++ b/src/PinguApps.Appwrite.Client/Clients/ISessionAware.cs @@ -1,12 +1,8 @@ -using System; - -namespace PinguApps.Appwrite.Client.Clients; +namespace PinguApps.Appwrite.Client.Clients; internal interface ISessionAware { public string? Session { get; protected set; } public void UpdateSession(string? session) => Session = session; - - event EventHandler? SessionChanged; } diff --git a/src/PinguApps.Appwrite.Client/Handlers/ClientCookieSessionHandler.cs b/src/PinguApps.Appwrite.Client/Handlers/ClientCookieSessionHandler.cs new file mode 100644 index 00000000..b6098f22 --- /dev/null +++ b/src/PinguApps.Appwrite.Client/Handlers/ClientCookieSessionHandler.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using PinguApps.Appwrite.Client.Internals; + +namespace PinguApps.Appwrite.Client.Handlers; +internal class ClientCookieSessionHandler : DelegatingHandler +{ + private readonly Lazy _appwriteClient; + + public ClientCookieSessionHandler(Lazy appwriteClient) + { + _appwriteClient = appwriteClient; + } + + private IAppwriteClient AppwriteClient => _appwriteClient.Value; + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var result = await base.SendAsync(request, cancellationToken); + + SaveSession(result); + + return result; + } + + private void SaveSession(HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + { + if (response.Headers.TryGetValues("Set-Cookie", out var values)) + { + var sessionCookie = values.FirstOrDefault(x => x.StartsWith("a_session", StringComparison.OrdinalIgnoreCase) && !x.Contains("legacy", StringComparison.OrdinalIgnoreCase)); + + if (sessionCookie is null) + return; + + var base64 = sessionCookie.Split('=')[1].Split(';')[0]; + + var decodedBytes = Convert.FromBase64String(base64); + var decoded = Encoding.UTF8.GetString(decodedBytes); + + var sessionData = JsonSerializer.Deserialize(decoded); + + if (sessionData is null) + return; + + AppwriteClient.SetSession(sessionData.Secret); + } + } + } +} diff --git a/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs b/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs index d0af20cf..fa6b34e2 100644 --- a/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs +++ b/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs @@ -22,14 +22,17 @@ public static class ServiceCollectionExtensions /// The service collection, enabling chaining public static IServiceCollection AddAppwriteClient(this IServiceCollection services, string projectId, string endpoint = "https://cloud.appwrite.io/v1", RefitSettings? refitSettings = null) { - services.AddSingleton(sp => new HeaderHandler(projectId)); + services.AddSingleton(x => new HeaderHandler(projectId)); + services.AddSingleton(); services.AddRefitClient(refitSettings) .ConfigureHttpClient(x => x.BaseAddress = new Uri(endpoint)) - .AddHttpMessageHandler(); + .AddHttpMessageHandler() + .AddHttpMessageHandler(); - services.AddSingleton(x => new AccountClient(x, true)); + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(x => new Lazy(() => x.GetRequiredService())); return services; } @@ -54,7 +57,7 @@ public static IServiceCollection AddAppwriteClientForServer(this IServiceCollect UseCookies = false }); - services.AddSingleton(x => new AccountClient(x, false)); + services.AddSingleton(); services.AddSingleton(); return services; diff --git a/src/PinguApps.Appwrite.Playground/App.cs b/src/PinguApps.Appwrite.Playground/App.cs index 7c148f93..6bc62e81 100644 --- a/src/PinguApps.Appwrite.Playground/App.cs +++ b/src/PinguApps.Appwrite.Playground/App.cs @@ -24,7 +24,7 @@ public async Task Run(string[] args) var request = new CreateSessionRequest { UserId = "664aac1a00113f82e620", - Secret = "814123" + Secret = "834938" }; Console.WriteLine($"Session: {_client.Session}"); From 2f14b2ce5d55c20e553714cf3048279ae9ceffd0 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Mon, 15 Jul 2024 03:03:24 +0100 Subject: [PATCH 09/16] Added create session tests --- .../Requests/CreateSessionRequestTests.cs | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 tests/PinguApps.Appwrite.Shared.Tests/Requests/CreateSessionRequestTests.cs diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Requests/CreateSessionRequestTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Requests/CreateSessionRequestTests.cs new file mode 100644 index 00000000..6884a1bc --- /dev/null +++ b/tests/PinguApps.Appwrite.Shared.Tests/Requests/CreateSessionRequestTests.cs @@ -0,0 +1,108 @@ + +using FluentValidation; +using PinguApps.Appwrite.Shared.Requests; + +namespace PinguApps.Appwrite.Shared.Tests.Requests; +public class CreateSessionRequestTests +{ + [Fact] + public void Constructor_InitializesWithExpectedValues() + { + // Arrange & Act + var request = new CreateSessionRequest(); + + // Assert + Assert.NotEmpty(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 CreateSessionRequest(); + + // 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 CreateSessionRequest + { + 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 CreateSessionRequest + { + UserId = userId, + Secret = secret + }; + + // Act + var isValid = request.IsValid(); + + // Assert + Assert.False(isValid); + } + + [Fact] + public void Validate_WithThrowOnFailuresTrue_ThrowsValidationExceptionOnFailure() + { + // Arrange + var request = new CreateSessionRequest + { + UserId = ".badChar^", + Secret = "" + }; + + // Assert + Assert.Throws(() => request.Validate(true)); + } + + [Fact] + public void Validate_WithThrowOnFailuresFalse_ReturnsInvalidResultOnFailure() + { + // Arrange + var request = new CreateSessionRequest + { + UserId = ".badChar^", + Secret = "" + }; + + // Act + var result = request.Validate(false); + + // Assert + Assert.False(result.IsValid); + } +} From ee8d7147229bc0f456eaaa8a43ae670707ff60d4 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Mon, 15 Jul 2024 03:04:32 +0100 Subject: [PATCH 10/16] Fixed tests, as we were failing many --- .../ServiceCollectionExtensions.cs | 7 +++-- src/PinguApps.Appwrite.Playground/Program.cs | 4 +-- .../ServiceCollectionExtensionsTests.cs | 28 +++++++++---------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs b/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs index fa6b34e2..d83ece5b 100644 --- a/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs +++ b/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs @@ -52,9 +52,12 @@ public static IServiceCollection AddAppwriteClientForServer(this IServiceCollect services.AddRefitClient(refitSettings) .ConfigureHttpClient(x => x.BaseAddress = new Uri(endpoint)) .AddHttpMessageHandler() - .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + .ConfigurePrimaryHttpMessageHandler((handler, sp) => { - UseCookies = false + if (handler is HttpClientHandler clientHandler) + { + clientHandler.UseCookies = false; + } }); services.AddSingleton(); diff --git a/src/PinguApps.Appwrite.Playground/Program.cs b/src/PinguApps.Appwrite.Playground/Program.cs index 2f280220..07824411 100644 --- a/src/PinguApps.Appwrite.Playground/Program.cs +++ b/src/PinguApps.Appwrite.Playground/Program.cs @@ -7,8 +7,8 @@ var builder = Host.CreateApplicationBuilder(args); -builder.Services.AddAppwriteClient(builder.Configuration.GetValue("ProjectId")!); -//builder.Services.AddAppwriteClientForServer(builder.Configuration.GetValue("ProjectId")!); +//builder.Services.AddAppwriteClient(builder.Configuration.GetValue("ProjectId")!); +builder.Services.AddAppwriteClientForServer(builder.Configuration.GetValue("ProjectId")!); builder.Services.AddAppwriteServer(builder.Configuration.GetValue("ProjectId")!, builder.Configuration.GetValue("ApiKey")!); builder.Services.AddSingleton(); diff --git a/tests/PinguApps.Appwrite.Client.Tests/ServiceCollectionExtensionsTests.cs b/tests/PinguApps.Appwrite.Client.Tests/ServiceCollectionExtensionsTests.cs index 1f93b2cb..f1d65270 100644 --- a/tests/PinguApps.Appwrite.Client.Tests/ServiceCollectionExtensionsTests.cs +++ b/tests/PinguApps.Appwrite.Client.Tests/ServiceCollectionExtensionsTests.cs @@ -18,21 +18,26 @@ public void AddAppwriteClient_RegistersExpectedServices() // Assert var provider = services.BuildServiceProvider(); - // Assert HeaderHandler is registered var headerHandler = provider.GetService(); Assert.NotNull(headerHandler); - // Assert IAccountApi is registered and configured + var clientCookieSessionHandler = provider.GetService(); + Assert.NotNull(clientCookieSessionHandler); + var accountApi = provider.GetService(); Assert.NotNull(accountApi); - // Assert services are registered Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); + + var lazyClient = provider.GetService>(); + Assert.NotNull(lazyClient); + var client = lazyClient.Value; + Assert.NotNull(client); } [Fact] - public void AddAppwriteClientForServer_RegistersExpectedServicesWithTransientLifetime() + public void AddAppwriteClientForServer_RegistersExpectedServices() { // Arrange var services = new ServiceCollection(); @@ -43,21 +48,16 @@ public void AddAppwriteClientForServer_RegistersExpectedServicesWithTransientLif // Assert var provider = services.BuildServiceProvider(); - // Assert HeaderHandler is registered var headerHandler = provider.GetService(); Assert.NotNull(headerHandler); - // Assert IAccountApi is registered and configured + var clientCookieSessionHandler = provider.GetService(); + Assert.Null(clientCookieSessionHandler); + var accountApi = provider.GetService(); Assert.NotNull(accountApi); - // Assert services are registered with Transient lifetime - var accountClientServiceDescriptor = services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(IAccountClient)); - Assert.NotNull(accountClientServiceDescriptor); - Assert.Equal(ServiceLifetime.Transient, accountClientServiceDescriptor.Lifetime); - - var appwriteClientServiceDescriptor = services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(IAppwriteClient)); - Assert.NotNull(appwriteClientServiceDescriptor); - Assert.Equal(ServiceLifetime.Transient, appwriteClientServiceDescriptor.Lifetime); + Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService()); } } From 1ebda0e78683d0925b6a748a35e85c967f2041dc Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Mon, 15 Jul 2024 03:08:34 +0100 Subject: [PATCH 11/16] added tests for client --- .../AccountClientTests.CreateSesssion.cs | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateSesssion.cs diff --git a/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateSesssion.cs b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateSesssion.cs new file mode 100644 index 00000000..9c285c37 --- /dev/null +++ b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateSesssion.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 CreateSession_ShouldReturnSuccess_WhenApiCallSucceeds() + { + // Arrange + var request = new CreateSessionRequest() + { + UserId = "123456", + Secret = "654321" + }; + + _mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/sessions/token") + .ExpectedHeaders() + .WithJsonContent(request) + .Respond(Constants.AppJson, Constants.SessionResponse); + + // Act + var result = await _appwriteClient.Account.CreateSession(request); + + // Assert + Assert.True(result.Success); + } + + [Fact] + public async Task CreateSession_ShouldHandleException_WhenApiCallFails() + { + // Arrange + var request = new CreateSessionRequest() + { + UserId = "123456", + Secret = "654321" + }; + + _mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/sessions/token") + .ExpectedHeaders() + .WithJsonContent(request) + .Respond(HttpStatusCode.BadRequest, Constants.AppJson, Constants.AppwriteError); + + // Act + var result = await _appwriteClient.Account.CreateSession(request); + + // Assert + Assert.True(result.IsError); + Assert.True(result.IsAppwriteError); + } + + [Fact] + public async Task CreateSession_ShouldReturnErrorResponse_WhenExceptionOccurs() + { + // Arrange + var request = new CreateSessionRequest() + { + UserId = "123456", + Secret = "654321" + }; + + _mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/sessions/token") + .ExpectedHeaders() + .WithJsonContent(request) + .Throw(new HttpRequestException("An error occurred")); + + // Act + var result = await _appwriteClient.Account.CreateSession(request); + + // Assert + Assert.False(result.Success); + Assert.True(result.IsInternalError); + Assert.Equal("An error occurred", result.Result.AsT2.Message); + } +} From a9f3a8f8660a6845c75dd2995f48858db9df4961 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Mon, 15 Jul 2024 03:09:28 +0100 Subject: [PATCH 12/16] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 49cb279b..6b5a6bb9 100644 --- a/README.md +++ b/README.md @@ -139,11 +139,11 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( ## ⌛ Progress ### Server & Client -![11 / 298](https://progress-bar.dev/11/?scale=298&suffix=%20/%20298&width=500) +![12 / 298](https://progress-bar.dev/12/?scale=298&suffix=%20/%20298&width=500) ### Server Only ![2 / 195](https://progress-bar.dev/2/?scale=195&suffix=%20/%20195&width=300) ### Client Only -![9 / 93](https://progress-bar.dev/9/?scale=93&suffix=%20/%2093&width=300) +![10 / 93](https://progress-bar.dev/10/?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 -![11 / 52](https://progress-bar.dev/11/?scale=52&suffix=%20/%2052&width=120) +![12 / 52](https://progress-bar.dev/12/?scale=52&suffix=%20/%2052&width=120) | Endpoint | Client | Server | |:-:|:-:|:-:| @@ -188,7 +188,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( | [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) | ⬛ | ❌ | -| [Create Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#createSession) | ⬛ | ❌ | +| [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) | ⬛ | ❌ | | [Delete Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#deleteSession) | ⬛ | ❌ | From 5b5a980edef3940cc58a686181449ad6e130c67e Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Mon, 15 Jul 2024 04:01:25 +0100 Subject: [PATCH 13/16] Added test for client cookie session handler --- .../Handlers/ClientCookieSessionHandler.cs | 4 +- .../ClientCookieSessionHandlerTests.cs | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/PinguApps.Appwrite.Client.Tests/Handlers/ClientCookieSessionHandlerTests.cs diff --git a/src/PinguApps.Appwrite.Client/Handlers/ClientCookieSessionHandler.cs b/src/PinguApps.Appwrite.Client/Handlers/ClientCookieSessionHandler.cs index b6098f22..536809bb 100644 --- a/src/PinguApps.Appwrite.Client/Handlers/ClientCookieSessionHandler.cs +++ b/src/PinguApps.Appwrite.Client/Handlers/ClientCookieSessionHandler.cs @@ -39,7 +39,9 @@ private void SaveSession(HttpResponseMessage response) if (sessionCookie is null) return; - var base64 = sessionCookie.Split('=')[1].Split(';')[0]; + var afterEquals = sessionCookie.IndexOf('=') + 1; + var semicolonIndex = sessionCookie.IndexOf(';', afterEquals); + var base64 = sessionCookie.Substring(afterEquals, semicolonIndex - afterEquals); var decodedBytes = Convert.FromBase64String(base64); var decoded = Encoding.UTF8.GetString(decodedBytes); diff --git a/tests/PinguApps.Appwrite.Client.Tests/Handlers/ClientCookieSessionHandlerTests.cs b/tests/PinguApps.Appwrite.Client.Tests/Handlers/ClientCookieSessionHandlerTests.cs new file mode 100644 index 00000000..e8dd4273 --- /dev/null +++ b/tests/PinguApps.Appwrite.Client.Tests/Handlers/ClientCookieSessionHandlerTests.cs @@ -0,0 +1,45 @@ +using System.Text; +using Moq; +using Moq.Protected; +using PinguApps.Appwrite.Client.Handlers; +using PinguApps.Appwrite.Client.Internals; + +namespace PinguApps.Appwrite.Client.Tests.Handlers; +public class ClientCookieSessionHandlerTests +{ + [Fact] + public async Task SendAsync_WhenResponseHasSessionCookie_SavesSessionCorrectly() + { + // Arrange + var mockInnerHandler = new Mock(); + var mockAppwriteClient = new Mock(); + var sessionData = new CookieSessionData { Id = "123456", Secret = "test_secret" }; + var encodedSessionData = Convert.ToBase64String(Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(sessionData))); + var cookieValue = $"a_session={encodedSessionData}; Path=/; HttpOnly"; + + mockInnerHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK, + Headers = { { "Set-Cookie", cookieValue } } + }) + .Verifiable(); + + var handler = new ClientCookieSessionHandler(new Lazy(() => mockAppwriteClient.Object)) + { + InnerHandler = mockInnerHandler.Object + }; + var httpClient = new HttpClient(handler); + + // Act + await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); + + // Assert + mockAppwriteClient.Verify(client => client.SetSession("test_secret"), Times.Once); + } +} From f6bed6402b6b63cfd1e23373303f5b40803fc525 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Mon, 15 Jul 2024 04:42:57 +0100 Subject: [PATCH 14/16] Compelted client tests, and handling logging out of a session in client cookie session handler --- .../Handlers/ClientCookieSessionHandler.cs | 20 +- .../ClientCookieSessionHandlerTests.cs | 178 +++++++++++++++--- 2 files changed, 171 insertions(+), 27 deletions(-) diff --git a/src/PinguApps.Appwrite.Client/Handlers/ClientCookieSessionHandler.cs b/src/PinguApps.Appwrite.Client/Handlers/ClientCookieSessionHandler.cs index 536809bb..ba52b591 100644 --- a/src/PinguApps.Appwrite.Client/Handlers/ClientCookieSessionHandler.cs +++ b/src/PinguApps.Appwrite.Client/Handlers/ClientCookieSessionHandler.cs @@ -43,15 +43,27 @@ private void SaveSession(HttpResponseMessage response) var semicolonIndex = sessionCookie.IndexOf(';', afterEquals); var base64 = sessionCookie.Substring(afterEquals, semicolonIndex - afterEquals); + if (string.Equals(base64, "deleted", StringComparison.OrdinalIgnoreCase)) + { + AppwriteClient.SetSession(null); + return; + } + var decodedBytes = Convert.FromBase64String(base64); var decoded = Encoding.UTF8.GetString(decodedBytes); - var sessionData = JsonSerializer.Deserialize(decoded); + try + { + var sessionData = JsonSerializer.Deserialize(decoded); - if (sessionData is null) - return; + if (sessionData is null || sessionData.Id is null || sessionData.Secret is null) + return; - AppwriteClient.SetSession(sessionData.Secret); + AppwriteClient.SetSession(sessionData.Secret); + } + catch (JsonException) + { + } } } } diff --git a/tests/PinguApps.Appwrite.Client.Tests/Handlers/ClientCookieSessionHandlerTests.cs b/tests/PinguApps.Appwrite.Client.Tests/Handlers/ClientCookieSessionHandlerTests.cs index e8dd4273..cc993722 100644 --- a/tests/PinguApps.Appwrite.Client.Tests/Handlers/ClientCookieSessionHandlerTests.cs +++ b/tests/PinguApps.Appwrite.Client.Tests/Handlers/ClientCookieSessionHandlerTests.cs @@ -1,45 +1,177 @@ using System.Text; using Moq; -using Moq.Protected; using PinguApps.Appwrite.Client.Handlers; using PinguApps.Appwrite.Client.Internals; +using RichardSzalay.MockHttp; namespace PinguApps.Appwrite.Client.Tests.Handlers; public class ClientCookieSessionHandlerTests { + private readonly MockHttpMessageHandler _mockHttp; + private readonly Mock _mockAppwriteClient; + private readonly HttpClient _httpClient; + + public ClientCookieSessionHandlerTests() + { + _mockHttp = new MockHttpMessageHandler(); + _mockAppwriteClient = new Mock(); + var handler = new ClientCookieSessionHandler(new Lazy(() => _mockAppwriteClient.Object)) + { + InnerHandler = _mockHttp + }; + _httpClient = new HttpClient(handler); + } + [Fact] public async Task SendAsync_WhenResponseHasSessionCookie_SavesSessionCorrectly() { // Arrange - var mockInnerHandler = new Mock(); - var mockAppwriteClient = new Mock(); var sessionData = new CookieSessionData { Id = "123456", Secret = "test_secret" }; var encodedSessionData = Convert.ToBase64String(Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(sessionData))); var cookieValue = $"a_session={encodedSessionData}; Path=/; HttpOnly"; - mockInnerHandler.Protected() - .Setup>( - "SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny() - ) - .ReturnsAsync(new HttpResponseMessage - { - StatusCode = System.Net.HttpStatusCode.OK, - Headers = { { "Set-Cookie", cookieValue } } - }) - .Verifiable(); - - var handler = new ClientCookieSessionHandler(new Lazy(() => mockAppwriteClient.Object)) - { - InnerHandler = mockInnerHandler.Object - }; - var httpClient = new HttpClient(handler); + _mockHttp.When(HttpMethod.Get, "http://test.com") + .Respond(req => + new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK, + Headers = { { "Set-Cookie", cookieValue } } + }); + + // Act + await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); + + // Assert + _mockAppwriteClient.Verify(client => client.SetSession("test_secret"), Times.Once); + } + + [Fact] + public async Task SendAsync_NoSetCookieHeader_DoesNotSetSession() + { + // Arrange + + _mockHttp.When(HttpMethod.Get, "http://test.com") + .Respond(req => + new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK + }); + + // Act + await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); + + // Assert + _mockAppwriteClient.Verify(client => client.SetSession(It.IsAny()), Times.Never); + } + + [Fact] + public async Task SendAsync_NoASessionCookie_DoesNotSetSession() + { + // Arrange + _mockHttp.When(HttpMethod.Get, "http://test.com") + .Respond(req => + new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK, + Headers = { { "Set-Cookie", "not_a_session=abc123; Path=/; HttpOnly" } } + }); + + // Act + await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); + + // Assert + _mockAppwriteClient.Verify(client => client.SetSession(It.IsAny()), Times.Never); + } + + [Fact] + public async Task SendAsync_InvalidBase64InASessionCookie_DoesNotSetSession() + { + // Arrange + var invalidBase64 = "not_base64"; + _mockHttp.When(HttpMethod.Get, "http://test.com") + .Respond(req => + new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK, + Headers = { { "Set-Cookie", $"not_a_session={invalidBase64}; Path=/; HttpOnly" } } + }); // Act - await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); + await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); // Assert - mockAppwriteClient.Verify(client => client.SetSession("test_secret"), Times.Once); + _mockAppwriteClient.Verify(client => client.SetSession(It.IsAny()), Times.Never); } + + [Fact] + public async Task SendAsync_InvalidJsonInDecodedBase64_DoesNotSetSession() + { + // Arrange + var invalidJsonBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("{\"SomeKey\": \"SomeValue\"}")); + _mockHttp.When(HttpMethod.Get, "http://test.com") + .Respond(req => + new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK, + Headers = { { "Set-Cookie", $"a_session={invalidJsonBase64}; Path=/; HttpOnly" } } + }); + + // Act + await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); + + // Assert + _mockAppwriteClient.Verify(client => client.SetSession(It.IsAny()), Times.Never); + } + + [Fact] + public async Task SendAsync_SessionCookieMarkedAsDeleted_ClearsSession() + { + // Arrange + var deletedSessionCookie = "a_session=deleted; Path=/; HttpOnly"; + _mockHttp.When(HttpMethod.Get, "http://test.com") + .Respond(req => + new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK, + Headers = { { "Set-Cookie", deletedSessionCookie } } + }); + + // Act + await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); + + // Assert + _mockAppwriteClient.Verify(client => client.SetSession(null), Times.Once); + } + + [Fact] + public async Task SendAsync_InvalidJsonInSessionCookie_DoesNotThrowJsonException() + { + // Arrange + var invalidJsonBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("invalid_json")); + var invalidJsonSessionCookie = $"a_session={invalidJsonBase64}; Path=/; HttpOnly"; + _mockHttp.When(HttpMethod.Get, "http://test.com") + .Respond(req => + new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK, + Headers = { { "Set-Cookie", invalidJsonSessionCookie } } + }); + + Exception? caughtException = null; + + try + { + // Act + await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); + } + catch (Exception ex) + { + caughtException = ex; + } + + // Assert + Assert.Null(caughtException); + _mockAppwriteClient.Verify(client => client.SetSession(It.IsAny()), Times.Never); + } + } From b0e709c35604a1be859324d9279baeaa8e267d4b Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Mon, 15 Jul 2024 05:11:00 +0100 Subject: [PATCH 15/16] Shared now has 100% coverage --- .../Converters/NullableDateTimeConverter.cs | 9 -- .../NullableDateTimeConverterTests.cs | 93 +++++++++++++++++++ 2 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 tests/PinguApps.Appwrite.Shared.Tests/Converters/NullableDateTimeConverterTests.cs diff --git a/src/PinguApps.Appwrite.Shared/Converters/NullableDateTimeConverter.cs b/src/PinguApps.Appwrite.Shared/Converters/NullableDateTimeConverter.cs index eddda37f..2719aeca 100644 --- a/src/PinguApps.Appwrite.Shared/Converters/NullableDateTimeConverter.cs +++ b/src/PinguApps.Appwrite.Shared/Converters/NullableDateTimeConverter.cs @@ -23,11 +23,6 @@ internal class NullableDateTimeConverter : JsonConverter throw new JsonException($"Unable to parse '{stringValue}' to DateTime."); } - if (reader.TokenType == JsonTokenType.Null) - { - return null; - } - throw new JsonException("Unexpected token type."); } @@ -37,9 +32,5 @@ public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerialize { writer.WriteStringValue(value.Value.ToString("o")); } - else - { - writer.WriteNullValue(); - } } } diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Converters/NullableDateTimeConverterTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Converters/NullableDateTimeConverterTests.cs new file mode 100644 index 00000000..2c0fb17a --- /dev/null +++ b/tests/PinguApps.Appwrite.Shared.Tests/Converters/NullableDateTimeConverterTests.cs @@ -0,0 +1,93 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Converters; + +namespace PinguApps.Appwrite.Shared.Tests.Converters; + +public class NullableDateTimeConverterTests +{ + private readonly JsonSerializerOptions _options; + + public NullableDateTimeConverterTests() + { + _options = new JsonSerializerOptions(); + _options.Converters.Add(new NullableDateTimeConverter()); + } + + [Fact] + public void Read_ValidDateString_ReturnsDateTime() + { + var json = "\"2023-01-01T00:00:00\""; + var result = JsonSerializer.Deserialize(json, _options); + Assert.NotNull(result); + Assert.Equal(new DateTime(2023, 1, 1), result.Value); + } + + [Fact] + public void Read_EmptyString_ReturnsNull() + { + var json = "\"\""; + var result = JsonSerializer.Deserialize(json, _options); + Assert.Null(result); + } + + [Fact] + public void Read_NullToken_ReturnsNull() + { + var json = "null"; + var result = JsonSerializer.Deserialize(json, _options); + Assert.Null(result); + } + + public class NullableDateTimeObject + { + [JsonPropertyName("x")] + [JsonConverter(typeof(NullableDateTimeConverter))] + public DateTime? X { get; set; } + } + + [Fact] + public void Read_NullTokenInObject_ReturnsNull() + { + var json = "{\"x\": null}"; + var result = JsonSerializer.Deserialize(json, _options); + Assert.NotNull(result); + Assert.Null(result.X); + } + + [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_NonNullDateTime_WritesExpectedString() + { + var dateTime = new DateTime(2023, 1, 1); + var json = JsonSerializer.Serialize(dateTime, _options); + Assert.Equal("\"2023-01-01T00:00:00.0000000\"", json); + } + + [Fact] + public void Write_NullDateTime_WritesNullValue() + { + var json = JsonSerializer.Serialize(null, _options); + Assert.Equal("null", json); + } + + [Fact] + public void Write_NullDateTimeInObject_WritesNullValue() + { + var json = JsonSerializer.Serialize(new NullableDateTimeObject(), _options); + Assert.Equal("{\"x\":null}", json); + } +} From be7db6bd0fa290567c39ab9bd87fbde833a44bcf Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Mon, 15 Jul 2024 05:12:37 +0100 Subject: [PATCH 16/16] Update tests/PinguApps.Appwrite.Client.Tests/Handlers/ClientCookieSessionHandlerTests.cs Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com> --- .../Handlers/ClientCookieSessionHandlerTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/PinguApps.Appwrite.Client.Tests/Handlers/ClientCookieSessionHandlerTests.cs b/tests/PinguApps.Appwrite.Client.Tests/Handlers/ClientCookieSessionHandlerTests.cs index cc993722..ac7ad142 100644 --- a/tests/PinguApps.Appwrite.Client.Tests/Handlers/ClientCookieSessionHandlerTests.cs +++ b/tests/PinguApps.Appwrite.Client.Tests/Handlers/ClientCookieSessionHandlerTests.cs @@ -173,5 +173,4 @@ public async Task SendAsync_InvalidJsonInSessionCookie_DoesNotThrowJsonException Assert.Null(caughtException); _mockAppwriteClient.Verify(client => client.SetSession(It.IsAny()), Times.Never); } - }