about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs1
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs9
-rw-r--r--LibMatrix/Extensions/MatrixHttpClient.Single.cs3
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs107
-rw-r--r--LibMatrix/Homeservers/RemoteHomeServer.cs1
-rw-r--r--LibMatrix/RoomTypes/GenericRoom.cs26
-rw-r--r--Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs16
-rw-r--r--Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs56
-rw-r--r--Tests/LibMatrix.Tests/Tests/AuthTests.cs27
-rw-r--r--Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs14
-rw-r--r--Tests/LibMatrix.Tests/Tests/TestCleanup.cs69
11 files changed, 210 insertions, 119 deletions
diff --git a/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs b/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
index 6006048..89e2fdb 100644
--- a/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
+++ b/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
@@ -79,6 +79,7 @@ public abstract class PolicyRuleEventContent : EventContent {
     /// </summary>
     [JsonPropertyName("gay.rory.matrix_room_utils.readable_expiry_time_utc")]
     [FriendlyName(Name = "Expires at")]
+    [TableHide]
     public DateTime? ExpiryDateTime {
         get => Expiry == null ? null : DateTimeOffset.FromUnixTimeMilliseconds(Expiry.Value).DateTime;
         set {
diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs
index 49a1b62..eb156b3 100644
--- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs
+++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs
@@ -57,12 +57,13 @@ public class RoomPowerLevelEventContent : EventContent {
         return Users.TryGetValue(userId, out var level) && level >= Events.GetValueOrDefault(eventType, EventsDefault ?? 0);
     }
 
-    public bool UserHasStatePermission(string userId, string eventType) {
+    public bool UserHasStatePermission(string userId, string eventType, bool log = false) {
         ArgumentNullException.ThrowIfNull(userId);
         var userLevel = GetUserPowerLevel(userId);
         var eventLevel = GetStateEventPowerLevel(eventType);
-        
-        Console.WriteLine($"{userId}={userLevel} >= {eventType}={eventLevel} = {userLevel >= eventLevel}");
+
+        if (log)
+            Console.WriteLine($"{userId}={userLevel} >= {eventType}={eventLevel} = {userLevel >= eventLevel}");
 
         return userLevel >= eventLevel;
     }
@@ -78,7 +79,7 @@ public class RoomPowerLevelEventContent : EventContent {
         if (Events is null) return StateDefault ?? 0;
         return Events.TryGetValue(eventType, out var level) ? level : StateDefault ?? 0;
     }
-    
+
     public long GetTimelineEventPowerLevel(string eventType) {
         ArgumentNullException.ThrowIfNull(eventType);
         if (Events is null) return EventsDefault ?? 0;
diff --git a/LibMatrix/Extensions/MatrixHttpClient.Single.cs b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
index c9cd260..39eb7e5 100644
--- a/LibMatrix/Extensions/MatrixHttpClient.Single.cs
+++ b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
@@ -26,7 +26,8 @@ public class MatrixHttpClient {
                 EnableMultipleHttp2Connections = true
             };
             Client = new HttpClient(handler) {
-                DefaultRequestVersion = new Version(3, 0)
+                DefaultRequestVersion = new Version(3, 0),
+                Timeout = TimeSpan.FromDays(1)
             };
         }
         catch (PlatformNotSupportedException e) {
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index c729a44..6be49b9 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -406,4 +406,111 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
         public NamedFilterCache FilterCache { get; init; }
         public NamedFileCache FileCache { get; init; }
     }
+
+#region Authenticated Media
+
+    // TODO: implement /_matrix/client/v1/media/config when it's actually useful - https://spec.matrix.org/v1.11/client-server-api/#get_matrixclientv1mediaconfig
+
+    private (string ServerName, string MediaId) ParseMxcUri(string mxcUri) {
+        if (!mxcUri.StartsWith("mxc://")) throw new ArgumentException("Matrix Content URIs must start with 'mxc://'", nameof(mxcUri));
+        var parts = mxcUri[6..].Split('/');
+        if (parts.Length != 2) throw new ArgumentException($"Invalid Matrix Content URI '{mxcUri}' passed! Matrix Content URIs must exist of only 2 parts!", nameof(mxcUri));
+        return (parts[0], parts[1]);
+    }
+
+    public async Task<Stream> GetMediaStreamAsync(string mxcUri, string? filename = null, int? timeout = null) {
+        var (serverName, mediaId) = ParseMxcUri(mxcUri);
+        try {
+            var uri = $"/_matrix/client/v1/media/download/{serverName}/{mediaId}";
+            if (!string.IsNullOrWhiteSpace(filename)) uri += $"/{HttpUtility.UrlEncode(filename)}";
+            if (timeout is not null) uri += $"?timeout_ms={timeout}";
+            var res = await ClientHttpClient.GetAsync(uri);
+            return await res.Content.ReadAsStreamAsync();
+        }
+        catch (MatrixException e) {
+            if (e is not { ErrorCode: "M_UNKNOWN" }) throw;
+        }
+
+        //fallback to legacy media
+        try {
+            var uri = $"/_matrix/media/v1/download/{serverName}/{mediaId}";
+            if (!string.IsNullOrWhiteSpace(filename)) uri += $"/{HttpUtility.UrlEncode(filename)}";
+            if (timeout is not null) uri += $"?timeout_ms={timeout}";
+            var res = await ClientHttpClient.GetAsync(uri);
+            return await res.Content.ReadAsStreamAsync();
+        }
+        catch (MatrixException e) {
+            if (e is not { ErrorCode: "M_UNKNOWN" }) throw;
+        }
+
+        throw new LibMatrixException() {
+            ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED,
+            Error = "Failed to download media"
+        };
+        // return default;
+    }
+
+    public async Task<Stream> GetThumbnailStreamAsync(string mxcUri, int width, int height, string? method = null, int? timeout = null) {
+        var (serverName, mediaId) = ParseMxcUri(mxcUri);
+        try {
+            var uri = new Uri($"/_matrix/client/v1/thumbnail/{serverName}/{mediaId}");
+            uri = uri.AddQuery("width", width.ToString());
+            uri = uri.AddQuery("height", height.ToString());
+            if (!string.IsNullOrWhiteSpace(method)) uri = uri.AddQuery("method", method);
+            if (timeout is not null) uri = uri.AddQuery("timeout_ms", timeout.ToString());
+
+            var res = await ClientHttpClient.GetAsync(uri.ToString());
+            return await res.Content.ReadAsStreamAsync();
+        }
+        catch (MatrixException e) {
+            if (e is not { ErrorCode: "M_UNKNOWN" }) throw;
+        }
+
+        //fallback to legacy media
+        try {
+            var uri = new Uri($"/_matrix/media/v1/thumbnail/{serverName}/{mediaId}");
+            uri = uri.AddQuery("width", width.ToString());
+            uri = uri.AddQuery("height", height.ToString());
+            if (!string.IsNullOrWhiteSpace(method)) uri = uri.AddQuery("method", method);
+            if (timeout is not null) uri = uri.AddQuery("timeout_ms", timeout.ToString());
+
+            var res = await ClientHttpClient.GetAsync(uri.ToString());
+            return await res.Content.ReadAsStreamAsync();
+        }
+        catch (MatrixException e) {
+            if (e is not { ErrorCode: "M_UNKNOWN" }) throw;
+        }
+
+        throw new LibMatrixException() {
+            ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED,
+            Error = "Failed to download media"
+        };
+        // return default;
+    }
+
+    public async Task<Dictionary<string, JsonValue>?> GetUrlPreviewAsync(string url) {
+        try {
+            var res = await ClientHttpClient.GetAsync($"/_matrix/client/v1/media/preview_url?url={HttpUtility.UrlEncode(url)}");
+            return await res.Content.ReadFromJsonAsync<Dictionary<string, JsonValue>>();
+        }
+        catch (MatrixException e) {
+            if (e is not { ErrorCode: "M_UNRECOGNIZED" }) throw;
+        }
+        
+        //fallback to legacy media
+        try {
+            var res = await ClientHttpClient.GetAsync($"/_matrix/media/v1/preview_url?url={HttpUtility.UrlEncode(url)}");
+            return await res.Content.ReadFromJsonAsync<Dictionary<string, JsonValue>>();
+        }
+        catch (MatrixException e) {
+            if (e is not { ErrorCode: "M_UNRECOGNIZED" }) throw;
+        }
+        
+        throw new LibMatrixException() {
+            ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED,
+            Error = "Failed to download URL preview"
+        };
+    }
+
+#endregion
 }
\ No newline at end of file
diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs
index ecf3e3a..f9e3d04 100644
--- a/LibMatrix/Homeservers/RemoteHomeServer.cs
+++ b/LibMatrix/Homeservers/RemoteHomeServer.cs
@@ -107,6 +107,7 @@ public class RemoteHomeserver {
 
 #endregion
 
+    [Obsolete("This call uses the deprecated unauthenticated media endpoints, please switch to the relevant AuthenticatedHomeserver methods instead.", true)]
     public string? ResolveMediaUri(string? mxcUri) {
         if (mxcUri is null) return null;
         if (mxcUri.StartsWith("https://")) return mxcUri;
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index b906f08..a1ef617 100644
--- a/LibMatrix/RoomTypes/GenericRoom.cs
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -210,10 +210,11 @@ public class GenericRoom {
     public async Task<RoomIdResponse> JoinAsync(string[]? homeservers = null, string? reason = null, bool checkIfAlreadyMember = true) {
         if (checkIfAlreadyMember)
             try {
-                _ = await GetCreateEventAsync();
-                return new RoomIdResponse {
-                    RoomId = RoomId
-                };
+                var ser = await GetStateEventOrNullAsync(RoomMemberEventContent.EventId, Homeserver.UserId);
+                if (ser?.TypedContent is RoomMemberEventContent { Membership: "join" })
+                    return new RoomIdResponse {
+                        RoomId = RoomId
+                    };
             }
             catch { } //ignore
 
@@ -316,6 +317,7 @@ public class GenericRoom {
     public Task<RoomPowerLevelEventContent?> GetPowerLevelsAsync() =>
         GetStateAsync<RoomPowerLevelEventContent>("m.room.power_levels");
 
+    [Obsolete("This method will be merged into GetNameAsync() in the future.")]
     public async Task<string> GetNameOrFallbackAsync(int maxMemberNames = 2) {
         try {
             return await GetNameAsync();
@@ -352,22 +354,6 @@ public class GenericRoom {
         return Task.WhenAll(tasks);
     }
 
-    public async Task<string?> GetResolvedRoomAvatarUrlAsync(bool useOriginHomeserver = false) {
-        var avatar = await GetAvatarUrlAsync();
-        if (avatar?.Url is null) return null;
-        if (!avatar.Url.StartsWith("mxc://")) return avatar.Url;
-        if (useOriginHomeserver)
-            try {
-                var hs = avatar.Url.Split('/', 3)[1];
-                return await new HomeserverResolverService(NullLogger<HomeserverResolverService>.Instance).ResolveMediaUri(hs, avatar.Url);
-            }
-            catch (Exception e) {
-                Console.WriteLine(e);
-            }
-
-        return Homeserver.ResolveMediaUri(avatar.Url);
-    }
-
 #endregion
 
 #region Simple calls
diff --git a/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs
index 401223c..2819f80 100644
--- a/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs
+++ b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs
@@ -78,19 +78,7 @@ public class HomeserverAbstraction(HomeserverProviderService _hsProvider, Config
         
         var username = _config.TestUsername;
         var password = _config.TestPassword;
-        
-        LoginResponse reg;
-        try {
-            reg = await rhs.LoginAsync(username, password);
-        }
-        catch (MatrixException e) {
-            if (e.ErrorCode == "M_FORBIDDEN") {
-                await rhs.RegisterAsync(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "Unit tests!");
-                reg = await rhs.RegisterAsync(username, password, "Unit tests!");
-            }
-            else throw new Exception("Failed to log in", e);
-        }
-        
-        return (username, password, reg.AccessToken);
+        var reg = await rhs.RegisterAsync(username, password, "Unit tests!");
+        return ("", "", "");
     }
 }
\ No newline at end of file
diff --git a/Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs b/Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs
new file mode 100644
index 0000000..712e45a
--- /dev/null
+++ b/Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs
@@ -0,0 +1,56 @@
+using ArcaneLibs.Extensions;
+using ArcaneLibs.Extensions.Streams;
+using LibMatrix.Homeservers;
+using LibMatrix.Services;
+using LibMatrix.Tests.Abstractions;
+using LibMatrix.Tests.Fixtures;
+using Xunit.Abstractions;
+using Xunit.Microsoft.DependencyInjection.Abstracts;
+
+namespace LibMatrix.Tests.Tests;
+
+public class AuthMediaTests : TestBed<TestFixture> {
+    private readonly TestFixture _fixture;
+    private readonly HomeserverResolverService _resolver;
+    private readonly Config _config;
+    private readonly HomeserverProviderService _provider;
+    private readonly HomeserverAbstraction _hsAbstraction;
+
+    public AuthMediaTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) {
+        _fixture = fixture;
+        _resolver = _fixture.GetService<HomeserverResolverService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}");
+        _config = _fixture.GetService<Config>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}");
+        _provider = _fixture.GetService<HomeserverProviderService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}");
+        _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}");
+    }
+
+    [Fact]
+    public async Task UploadFileAsync() {
+        var hs = await _hsAbstraction.GetConfiguredHomeserver();
+
+        var mxcUri = await hs.UploadFile("test", "LibMatrix test file".AsBytes());
+        Assert.NotNull(mxcUri);
+    }
+    
+    [Fact]
+    public async Task DownloadFileAsync() {
+        var hs = await _hsAbstraction.GetConfiguredHomeserver();
+
+        var mxcUri = await hs.UploadFile("test", "LibMatrix test file".AsBytes());
+        Assert.NotNull(mxcUri);
+        
+        var file = await hs.GetMediaStreamAsync(mxcUri);
+        Assert.NotNull(file);
+        
+        var data = file!.ReadToEnd().AsString();
+        Assert.Equal("LibMatrix test file", data);
+    }
+    
+    [SkippableFact(typeof(LibMatrixException))] // This test will fail if the homeserver does not support URL previews
+    public async Task GetUrlPreviewAsync() {
+        var hs = await _hsAbstraction.GetConfiguredHomeserver();
+        var preview = await hs.GetUrlPreviewAsync("https://matrix.org");
+        
+        Assert.NotNull(preview);
+    }
+}
\ No newline at end of file
diff --git a/Tests/LibMatrix.Tests/Tests/AuthTests.cs b/Tests/LibMatrix.Tests/Tests/AuthTests.cs
index 633c842..69e6231 100644
--- a/Tests/LibMatrix.Tests/Tests/AuthTests.cs
+++ b/Tests/LibMatrix.Tests/Tests/AuthTests.cs
@@ -20,17 +20,36 @@ public class AuthTests : TestBed<TestFixture> {
     
     [Fact]
     public async Task LoginWithPassword() {
-        var credentials = await _hsAbstraction.GetKnownCredentials();
+        Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver), $"{nameof(_config.TestHomeserver)} must be set in appsettings!");
+        Assert.False(string.IsNullOrWhiteSpace(_config.TestUsername), $"{nameof(_config.TestUsername)} must be set in appsettings!");
+        Assert.False(string.IsNullOrWhiteSpace(_config.TestPassword), $"{nameof(_config.TestPassword)} must be set in appsettings!");
+
+        // var server = await _resolver.ResolveHomeserverFromWellKnown(_config.TestHomeserver!);
+        var rhs = await _provider.GetRemoteHomeserver(_config.TestHomeserver);
+        var username = Guid.NewGuid().ToString();
+        var password = Guid.NewGuid().ToString();
         
-        var login = await _provider.Login(_config.TestHomeserver!, credentials.username, credentials.password);
+        var reg = await rhs.RegisterAsync(username, password, "Unit tests!");
+        
+        var login = await _provider.Login(_config.TestHomeserver!, username, password);
         Assert.NotNull(login);
         Assert.NotNull(login.AccessToken);
     }
 
     [Fact]
     public async Task LoginWithToken() {
-        var credentials = await _hsAbstraction.GetKnownCredentials();
-        var hs = await _provider.GetAuthenticatedWithToken(_config.TestHomeserver!, credentials.token);
+        Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver), $"{nameof(_config.TestHomeserver)} must be set in appsettings!");
+        Assert.False(string.IsNullOrWhiteSpace(_config.TestUsername), $"{nameof(_config.TestUsername)} must be set in appsettings!");
+        Assert.False(string.IsNullOrWhiteSpace(_config.TestPassword), $"{nameof(_config.TestPassword)} must be set in appsettings!");
+
+        // var server = await _resolver.ResolveHomeserverFromWellKnown(_config.TestHomeserver!);
+        var rhs = await _provider.GetRemoteHomeserver(_config.TestHomeserver);
+        var username = Guid.NewGuid().ToString();
+        var password = Guid.NewGuid().ToString();
+        
+        var reg = await rhs.RegisterAsync(username, password, "Unit tests!");
+        
+        var hs = await _provider.GetAuthenticatedWithToken(_config.TestHomeserver!, reg.AccessToken);
         Assert.NotNull(hs);
         Assert.NotNull(hs.WhoAmI);
         hs.WhoAmI.VerifyRequiredFields();
diff --git a/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs b/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs
index 03f3c24..20f975e 100644
--- a/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs
+++ b/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs
@@ -18,13 +18,13 @@ public class RemoteHomeserverTests : TestBed<TestFixture> {
         _provider = _fixture.GetService<HomeserverProviderService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}");
     }
 
-    [Fact]
-    public async Task ResolveMedia() {
-        var hs = await _provider.GetRemoteHomeserver("matrix.org");
-        var media = hs.ResolveMediaUri("mxc://matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo");
-        
-        Assert.Equal("https://matrix-client.matrix.org/_matrix/media/v3/download/matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo", media);
-    }
+    // [Fact]
+    // public async Task ResolveMedia() {
+    //     var hs = await _provider.GetRemoteHomeserver("matrix.org");
+    //     var media = hs.ResolveMediaUri("mxc://matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo");
+    //     
+    //     Assert.Equal("https://matrix-client.matrix.org/_matrix/media/v3/download/matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo", media);
+    // }
 
     [Fact]
     public async Task ResolveRoomAliasAsync() {
diff --git a/Tests/LibMatrix.Tests/Tests/TestCleanup.cs b/Tests/LibMatrix.Tests/Tests/TestCleanup.cs
deleted file mode 100644
index 1c5747c..0000000
--- a/Tests/LibMatrix.Tests/Tests/TestCleanup.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-// using System.Diagnostics;
-// using LibMatrix.Helpers;
-// using LibMatrix.Services;
-// using LibMatrix.Tests.Abstractions;
-// using LibMatrix.Tests.Fixtures;
-// using Microsoft.Extensions.Logging;
-// using Xunit.Abstractions;
-// using Xunit.Microsoft.DependencyInjection.Abstracts;
-//
-// namespace LibMatrix.Tests.Tests;
-//
-// public class TestCleanup : TestBed<TestFixture> {
-//     private readonly HomeserverAbstraction _hsAbstraction;
-//     private readonly ILogger<TestCleanup> _logger;
-//
-//     public TestCleanup(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) {
-//         // _fixture = fixture;
-//         _logger = _fixture.GetService<ILogger<TestCleanup>>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(ILogger<TestCleanup>)}");
-//         _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}");
-//     }
-//
-//     [SkippableFact(typeof(MatrixException))]
-//     public async Task Cleanup() {
-//         // Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver), $"{nameof(_config.TestHomeserver)} must be set in appsettings!");
-//         // Assert.False(string.IsNullOrWhiteSpace(_config.TestUsername), $"{nameof(_config.TestUsername)} must be set in appsettings!");
-//         // Assert.False(string.IsNullOrWhiteSpace(_config.TestPassword), $"{nameof(_config.TestPassword)} must be set in appsettings!");
-//
-//         var hs = await _hsAbstraction.GetConfiguredHomeserver();
-//         Assert.NotNull(hs);
-//
-//         var syncHelper = new SyncHelper(hs, _logger) {
-//             Timeout = 3000
-//         };
-//         _testOutputHelper.WriteLine("Starting sync loop");
-//         var cancellationTokenSource = new CancellationTokenSource();
-//         var sw = Stopwatch.StartNew();
-//         syncHelper.SyncReceivedHandlers.Add(async response => {
-//             // if (sw.ElapsedMilliseconds >= 3000) {
-//                 // _testOutputHelper.WriteLine("Cancelling sync loop");
-//
-//                 var tasks = (await hs.GetJoinedRooms()).Select(async room => {
-//                     _logger.LogInformation("Leaving room: {}", room.RoomId);
-//                     await room.LeaveAsync();
-//                     await room.ForgetAsync();
-//                     return room;
-//                 }).ToList();
-//                 await Task.WhenAll(tasks);
-//
-//                 // cancellationTokenSource.Cancel();
-//             // }
-//
-//             sw.Restart();
-//             if (response.Rooms?.Leave is { Count: > 0 }) {
-//                 // foreach (var room in response.Rooms.Leave) {
-//                 // await hs.GetRoom(room.Key).ForgetAsync();
-//                 // }
-//                 var tasks2 = response.Rooms.Leave.Select(async room => {
-//                     await hs.GetRoom(room.Key).ForgetAsync();
-//                     return room;
-//                 }).ToList();
-//                 await Task.WhenAll(tasks2);
-//             }
-//         });
-//         await syncHelper.RunSyncLoopAsync(cancellationToken: cancellationTokenSource.Token);
-//
-//         Assert.NotNull(hs);
-//         await hs.Logout();
-//     }
-// }
\ No newline at end of file