From 4bdea63982dae9c17b7a5fbda38d505655b8d4b3 Mon Sep 17 00:00:00 2001 From: "Emma [it/its]@Rory&" Date: Thu, 30 May 2024 21:39:12 +0200 Subject: Changes --- ArcaneLibs | 2 +- LibMatrix/Events/BaseEvent.cs | 52 ++++ LibMatrix/Extensions/HttpClientExtensions.cs | 248 ++++++++++++++-- LibMatrix/Extensions/JsonConverters.cs | 29 ++ LibMatrix/Extensions/JsonElementExtensions.cs | 147 ---------- .../Homeservers/AuthenticatedHomeserverGeneric.cs | 2 + LibMatrix/Homeservers/FederationClient.cs | 2 +- LibMatrix/Homeservers/RemoteHomeServer.cs | 2 +- LibMatrix/Interfaces/Services/IStorageProvider.cs | 56 ---- LibMatrix/LibMatrix.csproj | 6 +- LibMatrix/RoomTypes/GenericRoom.cs | 47 +-- LibMatrix/RoomTypes/SpaceRoom.cs | 5 +- LibMatrix/Services/HomeserverResolverService.cs | 2 +- LibMatrix/Services/TieredStorageService.cs | 8 - LibMatrix/StateEvent.cs | 6 + LibMatrix/UserIdAndReason.cs | 2 +- .../LibMatrix.HomeserverEmulator.csproj | 5 +- .../Abstractions/HomeserverAbstraction.cs | 111 ++++--- .../Abstractions/RoomAbstraction.cs | 9 +- Tests/LibMatrix.Tests/Config.cs | 37 ++- Tests/LibMatrix.Tests/Fixtures/TestFixture.cs | 28 +- Tests/LibMatrix.Tests/LibMatrix.Tests.csproj | 8 +- Tests/LibMatrix.Tests/Tests/AuthTests.cs | 35 +-- .../Tests/HomeserverResolverTests.cs | 43 +++ .../LibMatrix.Tests/Tests/RemoteHomeserverTests.cs | 56 ++++ Tests/LibMatrix.Tests/Tests/ResolverTest.cs | 55 ---- Tests/LibMatrix.Tests/Tests/RoomEventTests.cs | 154 ---------- Tests/LibMatrix.Tests/Tests/RoomTests.cs | 248 ---------------- .../BasicRoomEventTests/OtherRoomTests.cs | 101 +++++++ .../BasicRoomEventTests/RoomAvatarTests.cs | 27 ++ .../RoomTests/BasicRoomEventTests/RoomNameTests.cs | 42 +++ .../BasicRoomEventTests/RoomTopicTests.cs | 27 ++ .../Tests/RoomTests/RoomEventTests.cs | 150 ++++++++++ .../Tests/RoomTests/RoomMembershipTests.cs | 178 ++++++++++++ Tests/LibMatrix.Tests/Tests/RoomTests/RoomTests.cs | 320 +++++++++++++++++++++ .../LibMatrix.Tests/Tests/RoomTests/SpaceTests.cs | 101 +++++++ Tests/LibMatrix.Tests/Tests/TestCleanup.cs | 143 +++++---- Tests/TestDataGenerator/Program.cs | 12 +- .../JsonElementExtensions.cs | 147 ++++++++++ .../LibMatrix.DebugDataValidationApi.csproj | 4 +- .../Bot/FileStorageProvider.cs | 72 ++--- .../LibMatrix.Utilities.Bot/FileStorageProvider.cs | 64 ++--- .../LibMatrix.Utilities.Bot.csproj | 4 +- 43 files changed, 1824 insertions(+), 973 deletions(-) create mode 100644 LibMatrix/Events/BaseEvent.cs create mode 100644 LibMatrix/Extensions/JsonConverters.cs delete mode 100644 LibMatrix/Extensions/JsonElementExtensions.cs delete mode 100644 LibMatrix/Interfaces/Services/IStorageProvider.cs delete mode 100644 LibMatrix/Services/TieredStorageService.cs create mode 100644 Tests/LibMatrix.Tests/Tests/HomeserverResolverTests.cs create mode 100644 Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs delete mode 100644 Tests/LibMatrix.Tests/Tests/ResolverTest.cs delete mode 100644 Tests/LibMatrix.Tests/Tests/RoomEventTests.cs delete mode 100644 Tests/LibMatrix.Tests/Tests/RoomTests.cs create mode 100644 Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/OtherRoomTests.cs create mode 100644 Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomAvatarTests.cs create mode 100644 Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomNameTests.cs create mode 100644 Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomTopicTests.cs create mode 100644 Tests/LibMatrix.Tests/Tests/RoomTests/RoomEventTests.cs create mode 100644 Tests/LibMatrix.Tests/Tests/RoomTests/RoomMembershipTests.cs create mode 100644 Tests/LibMatrix.Tests/Tests/RoomTests/RoomTests.cs create mode 100644 Tests/LibMatrix.Tests/Tests/RoomTests/SpaceTests.cs create mode 100644 Utilities/LibMatrix.DebugDataValidationApi/JsonElementExtensions.cs diff --git a/ArcaneLibs b/ArcaneLibs index 68eca20..808f94b 160000 --- a/ArcaneLibs +++ b/ArcaneLibs @@ -1 +1 @@ -Subproject commit 68eca20fbf4d5c08b6960fb2362ba3733d2df9e5 +Subproject commit 808f94b5dc151e6659d2b13aa92302cc1a37b172 diff --git a/LibMatrix/Events/BaseEvent.cs b/LibMatrix/Events/BaseEvent.cs new file mode 100644 index 0000000..2e27368 --- /dev/null +++ b/LibMatrix/Events/BaseEvent.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace LibMatrix.Events; + +public class StateEvent { + [JsonPropertyName("state_key")] + public string? StateKey { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("replaces_state")] + public string? ReplacesState { get; set; } +} + +public class StateEventResponse : StateEvent { + [JsonPropertyName("origin_server_ts")] + public long? OriginServerTs { get; set; } + + [JsonPropertyName("room_id")] + public string? RoomId { get; set; } + + [JsonPropertyName("sender")] + public string? Sender { get; set; } + + [JsonPropertyName("unsigned")] + public UnsignedData? Unsigned { get; set; } + + [JsonPropertyName("event_id")] + public string? EventId { get; set; } + + public class UnsignedData { + [JsonPropertyName("age")] + public ulong? Age { get; set; } + + [JsonPropertyName("redacted_because")] + public object? RedactedBecause { get; set; } + + [JsonPropertyName("transaction_id")] + public string? TransactionId { get; set; } + + [JsonPropertyName("replaces_state")] + public string? ReplacesState { get; set; } + + [JsonPropertyName("prev_sender")] + public string? PrevSender { get; set; } + + [JsonPropertyName("prev_content")] + public JsonObject? PrevContent { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix/Extensions/HttpClientExtensions.cs b/LibMatrix/Extensions/HttpClientExtensions.cs index 64b4f6a..f801e16 100644 --- a/LibMatrix/Extensions/HttpClientExtensions.cs +++ b/LibMatrix/Extensions/HttpClientExtensions.cs @@ -1,8 +1,10 @@ +#define SINGLE_HTTPCLIENT // Use a single HttpClient instance for all MatrixHttpClient instances +// #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Net.Http.Headers; using System.Reflection; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -25,7 +27,16 @@ public static class HttpClientExtensions { } } -public class MatrixHttpClient : HttpClient { +#region Per-instance HTTP client code + +#if !SINGLE_HTTPCLIENT +public class MatrixHttpClient() : HttpClient(handler) { + private static readonly SocketsHttpHandler handler = new() { + PooledConnectionLifetime = TimeSpan.FromMinutes(15), + MaxConnectionsPerServer = 256, + EnableMultipleHttp2Connections = true + }; + public Dictionary AdditionalQueryParameters { get; set; } = new(); internal string? AssertedUserId { get; set; } @@ -44,7 +55,7 @@ public class MatrixHttpClient : HttpClient { public async Task SendUnhandledAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if(debug) await _rateLimitSemaphore.WaitAsync(cancellationToken); - // Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)})"); + Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)})"); if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null"); if (!request.RequestUri.IsAbsoluteUri) request.RequestUri = new Uri(BaseAddress, request.RequestUri); // if (AssertedUserId is not null) request.RequestUri = request.RequestUri.AddQuery("user_id", AssertedUserId); @@ -73,6 +84,8 @@ public class MatrixHttpClient : HttpClient { finally { if(debug) _rateLimitSemaphore.Release(); } + + Console.WriteLine($"Sending {request.Method} {request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}) -> {(int)responseMessage.StatusCode} {responseMessage.StatusCode} ({Util.BytesToString(responseMessage.Content.Headers.ContentLength ?? 0)})"); return responseMessage; } @@ -191,27 +204,220 @@ public class MatrixHttpClient : HttpClient { await foreach (var resp in result) yield return resp; } } +#endif -public class JsonFloatStringConverter : JsonConverter { - public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => float.Parse(reader.GetString()!); +#endregion - public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options) - => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); -} +#if SINGLE_HTTPCLIENT +public class MatrixHttpClient { + private static readonly SocketsHttpHandler handler; -public class JsonDoubleStringConverter : JsonConverter { - public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => double.Parse(reader.GetString()!); + private static readonly HttpClient client; - public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) - => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); -} + static MatrixHttpClient() { + try { + handler = new SocketsHttpHandler { + PooledConnectionLifetime = TimeSpan.FromMinutes(15), + MaxConnectionsPerServer = 4096, + EnableMultipleHttp2Connections = true + }; + client = new HttpClient(handler) { + DefaultRequestVersion = new Version(3, 0) + }; + } + catch (PlatformNotSupportedException e) { + Console.WriteLine("Failed to create HttpClient with connection pooling, continuing without connection pool!"); + Console.WriteLine("Original exception (safe to ignore!):"); + Console.WriteLine(e); + + client = new HttpClient { + DefaultRequestVersion = new Version(3, 0) + }; + } + catch (Exception e) { + Console.WriteLine("Failed to create HttpClient:"); + Console.WriteLine(e); + throw; + } + } + +#if SYNC_HTTPCLIENT + internal SemaphoreSlim _rateLimitSemaphore { get; } = new(1, 1); +#endif + + public Dictionary AdditionalQueryParameters { get; set; } = new(); + + public Uri? BaseAddress { get; set; } + + // default headers, not bound to client + public HttpRequestHeaders DefaultRequestHeaders { get; set; } = + typeof(HttpRequestHeaders).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[0], null)?.Invoke(new object[0]) as HttpRequestHeaders ?? + throw new InvalidOperationException("Failed to create HttpRequestHeaders"); + + private JsonSerializerOptions GetJsonSerializerOptions(JsonSerializerOptions? options = null) { + options ??= new JsonSerializerOptions(); + options.Converters.Add(new JsonFloatStringConverter()); + options.Converters.Add(new JsonDoubleStringConverter()); + options.Converters.Add(new JsonDecimalStringConverter()); + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + return options; + } + + public async Task SendUnhandledAsync(HttpRequestMessage request, CancellationToken cancellationToken) { +#if SYNC_HTTPCLIENT + await _rateLimitSemaphore.WaitAsync(cancellationToken); +#endif + + Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)})"); + + if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null"); + if (!request.RequestUri.IsAbsoluteUri) request.RequestUri = new Uri(BaseAddress, request.RequestUri); + foreach (var (key, value) in AdditionalQueryParameters) request.RequestUri = request.RequestUri.AddQuery(key, value); + foreach (var (key, value) in DefaultRequestHeaders) request.Headers.Add(key, value); + + request.Options.Set(new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"), true); -public class JsonDecimalStringConverter : JsonConverter { - public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => decimal.Parse(reader.GetString()!); + HttpResponseMessage? responseMessage; + try { + responseMessage = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + } + catch (Exception e) { + Console.WriteLine( + $"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}"); + throw; + } +#if SYNC_HTTPCLIENT + finally { + _rateLimitSemaphore.Release(); + } +#endif + + Console.WriteLine( + $"Sending {request.Method} {request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}) -> {(int)responseMessage.StatusCode} {responseMessage.StatusCode} ({Util.BytesToString(responseMessage.Content.Headers.ContentLength ?? 0)})"); + + return responseMessage; + } + + public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { + var responseMessage = await SendUnhandledAsync(request, cancellationToken); + if (responseMessage.IsSuccessStatusCode) return responseMessage; - public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options) - => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); -} \ No newline at end of file + //error handling + var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken); + if (content.Length == 0) + throw new MatrixException() { + ErrorCode = "M_UNKNOWN", + Error = "Unknown error, server returned no content" + }; + if (!content.StartsWith('{')) throw new InvalidDataException("Encountered invalid data:\n" + content); + //we have a matrix error + + MatrixException? ex = null; + try { + ex = JsonSerializer.Deserialize(content); + } + catch (JsonException e) { + throw new LibMatrixException() { + ErrorCode = "M_INVALID_JSON", + Error = e.Message + "\nBody:\n" + await responseMessage.Content.ReadAsStringAsync(cancellationToken) + }; + } + + Debug.Assert(ex != null, nameof(ex) + " != null"); + ex.RawContent = content; + // Console.WriteLine($"Failed to send request: {ex}"); + if (ex?.RetryAfterMs is null) throw ex!; + //we have a ratelimit error + await Task.Delay(ex.RetryAfterMs.Value, cancellationToken); + typeof(HttpRequestMessage).GetField("_sendStatus", BindingFlags.NonPublic | BindingFlags.Instance) + ?.SetValue(request, 0); + return await SendAsync(request, cancellationToken); + } + + // GetAsync + public Task GetAsync([StringSyntax("Uri")] string? requestUri, CancellationToken? cancellationToken = null) => + SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken ?? CancellationToken.None); + + // GetFromJsonAsync + public async Task TryGetFromJsonAsync(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { + try { + return await GetFromJsonAsync(requestUri, options, cancellationToken); + } + catch (HttpRequestException e) { + Console.WriteLine($"Failed to get {requestUri}: {e.Message}"); + return default; + } + } + + public async Task GetFromJsonAsync(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { + options = GetJsonSerializerOptions(options); + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var response = await SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); + + return await JsonSerializer.DeserializeAsync(responseStream, options, cancellationToken) ?? + throw new InvalidOperationException("Failed to deserialize response"); + } + + // GetStreamAsync + public new async Task GetStreamAsync(string requestUri, CancellationToken cancellationToken = default) { + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var response = await SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStreamAsync(cancellationToken); + } + + public async Task PutAsJsonAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) where T : notnull { + options = GetJsonSerializerOptions(options); + var request = new HttpRequestMessage(HttpMethod.Put, requestUri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options), + Encoding.UTF8, "application/json"); + return await SendAsync(request, cancellationToken); + } + + public async Task PostAsJsonAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) where T : notnull { + options ??= new JsonSerializerOptions(); + options.Converters.Add(new JsonFloatStringConverter()); + options.Converters.Add(new JsonDoubleStringConverter()); + options.Converters.Add(new JsonDecimalStringConverter()); + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options), + Encoding.UTF8, "application/json"); + return await SendAsync(request, cancellationToken); + } + + public async IAsyncEnumerable GetAsyncEnumerableFromJsonAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, JsonSerializerOptions? options = null) { + options = GetJsonSerializerOptions(options); + var res = await GetAsync(requestUri); + var result = JsonSerializer.DeserializeAsyncEnumerable(await res.Content.ReadAsStreamAsync(), options); + await foreach (var resp in result) yield return resp; + } + + public async Task CheckSuccessStatus(string url) { + //cors causes failure, try to catch + try { + var resp = await client.GetAsync(url); + return resp.IsSuccessStatusCode; + } + catch (Exception e) { + Console.WriteLine($"Failed to check success status: {e.Message}"); + return false; + } + } + + public async Task PostAsync(string uri, HttpContent? content, CancellationToken cancellationToken = default) { + var request = new HttpRequestMessage(HttpMethod.Post, uri) { + Content = content + }; + return await SendAsync(request, cancellationToken); + } +} +#endif \ No newline at end of file diff --git a/LibMatrix/Extensions/JsonConverters.cs b/LibMatrix/Extensions/JsonConverters.cs new file mode 100644 index 0000000..eed3fb2 --- /dev/null +++ b/LibMatrix/Extensions/JsonConverters.cs @@ -0,0 +1,29 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace LibMatrix.Extensions; + +public class JsonFloatStringConverter : JsonConverter { + public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => float.Parse(reader.GetString()!); + + public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); +} + +public class JsonDoubleStringConverter : JsonConverter { + public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => double.Parse(reader.GetString()!); + + public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); +} + +public class JsonDecimalStringConverter : JsonConverter { + public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => decimal.Parse(reader.GetString()!); + + public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); +} \ No newline at end of file diff --git a/LibMatrix/Extensions/JsonElementExtensions.cs b/LibMatrix/Extensions/JsonElementExtensions.cs deleted file mode 100644 index c4ed743..0000000 --- a/LibMatrix/Extensions/JsonElementExtensions.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - -namespace LibMatrix.Extensions; - -public static class JsonElementExtensions { - public static bool FindExtraJsonElementFields(this JsonElement obj, Type objectType, string objectPropertyName) { - if (objectPropertyName == "content" && objectType == typeof(JsonObject)) - objectType = typeof(StateEventResponse); - // if (t == typeof(JsonNode)) - // return false; - - Console.WriteLine($"{objectType.Name} {objectPropertyName}"); - var unknownPropertyFound = false; - var mappedPropsDict = objectType.GetProperties() - .Where(x => x.GetCustomAttribute() is not null) - .ToDictionary(x => x.GetCustomAttribute()!.Name, x => x); - objectType.GetProperties().Where(x => !mappedPropsDict.ContainsKey(x.Name)) - .ToList().ForEach(x => mappedPropsDict.TryAdd(x.Name, x)); - - foreach (var field in obj.EnumerateObject()) { - if (mappedPropsDict.TryGetValue(field.Name, out var mappedProperty)) { - //dictionary - if (mappedProperty.PropertyType.IsGenericType && - mappedProperty.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { - unknownPropertyFound |= _checkDictionary(field, objectType, mappedProperty.PropertyType); - continue; - } - - if (mappedProperty.PropertyType.IsGenericType && - mappedProperty.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) { - unknownPropertyFound |= _checkList(field, objectType, mappedProperty.PropertyType); - continue; - } - - if (field.Name == "content" && (objectType == typeof(StateEventResponse) || objectType == typeof(StateEvent))) { - unknownPropertyFound |= field.FindExtraJsonPropertyFieldsByValueKind( - StateEvent.GetStateEventType(obj.GetProperty("type").GetString()!), // We expect type to always be present - mappedProperty.PropertyType); - continue; - } - - unknownPropertyFound |= - field.FindExtraJsonPropertyFieldsByValueKind(objectType, mappedProperty.PropertyType); - continue; - } - - Console.WriteLine($"[!!] Unknown property {field.Name} in {objectType.Name}!"); - unknownPropertyFound = true; - } - - return unknownPropertyFound; - } - - private static bool FindExtraJsonPropertyFieldsByValueKind(this JsonProperty field, Type containerType, - Type propertyType) { - if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) propertyType = propertyType.GetGenericArguments()[0]; - - var switchResult = false; - switch (field.Value.ValueKind) { - case JsonValueKind.Array: - switchResult = field.Value.EnumerateArray().Aggregate(switchResult, - (current, element) => current | element.FindExtraJsonElementFields(propertyType, field.Name)); - break; - case JsonValueKind.Object: - switchResult |= field.Value.FindExtraJsonElementFields(propertyType, field.Name); - break; - case JsonValueKind.True: - case JsonValueKind.False: - return _checkBool(field, containerType, propertyType); - case JsonValueKind.String: - return _checkString(field, containerType, propertyType); - case JsonValueKind.Number: - return _checkNumber(field, containerType, propertyType); - case JsonValueKind.Undefined: - case JsonValueKind.Null: - break; - default: - throw new ArgumentOutOfRangeException(); - } - - return switchResult; - } - - private static bool _checkBool(this JsonProperty field, Type containerType, Type propertyType) { - if (propertyType == typeof(bool)) return true; - Console.WriteLine( - $"[!!] Encountered bool for {field.Name} in {containerType.Name}, the class defines {propertyType.Name}!"); - return false; - } - - private static bool _checkString(this JsonProperty field, Type containerType, Type propertyType) { - if (propertyType == typeof(string)) return true; - // ReSharper disable once BuiltInTypeReferenceStyle - if (propertyType == typeof(String)) return true; - Console.WriteLine( - $"[!!] Encountered string for {field.Name} in {containerType.Name}, the class defines {propertyType.Name}!"); - return false; - } - - private static bool _checkNumber(this JsonProperty field, Type containerType, Type propertyType) { - if (propertyType == typeof(int) || - propertyType == typeof(double) || - propertyType == typeof(float) || - propertyType == typeof(decimal) || - propertyType == typeof(long) || - propertyType == typeof(short) || - propertyType == typeof(uint) || - propertyType == typeof(ulong) || - propertyType == typeof(ushort) || - propertyType == typeof(byte) || - propertyType == typeof(sbyte)) - return true; - Console.WriteLine( - $"[!!] Encountered number for {field.Name} in {containerType.Name}, the class defines {propertyType.Name}!"); - return false; - } - - private static bool _checkDictionary(this JsonProperty field, Type containerType, Type propertyType) { - var keyType = propertyType.GetGenericArguments()[0]; - var valueType = propertyType.GetGenericArguments()[1]; - valueType = Nullable.GetUnderlyingType(valueType) ?? valueType; - Console.WriteLine( - $"Encountered dictionary {field.Name} with key type {keyType.Name} and value type {valueType.Name}!"); - - return field.Value.EnumerateObject() - .Where(key => !valueType.IsPrimitive && valueType != typeof(string)) - .Aggregate(false, (current, key) => - current | key.FindExtraJsonPropertyFieldsByValueKind(containerType, valueType) - ); - } - - private static bool _checkList(this JsonProperty field, Type containerType, Type propertyType) { - var valueType = propertyType.GetGenericArguments()[0]; - valueType = Nullable.GetUnderlyingType(valueType) ?? valueType; - Console.WriteLine( - $"Encountered list {field.Name} with value type {valueType.Name}!"); - - return field.Value.EnumerateArray() - .Where(key => !valueType.IsPrimitive && valueType != typeof(string)) - .Aggregate(false, (current, key) => - current | key.FindExtraJsonElementFields(valueType, field.Name) - ); - } -} \ No newline at end of file diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs index c729a44..891cc00 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs @@ -117,6 +117,8 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { } public virtual async Task Logout() { + // var res = await ClientHttpClient.PostAsync("/_matrix/client/v3/logout", null); + // TODO: investigate var res = await ClientHttpClient.PostAsync("/_matrix/client/v3/logout", null); if (!res.IsSuccessStatusCode) { Console.WriteLine($"Failed to logout: {await res.Content.ReadAsStringAsync()}"); diff --git a/LibMatrix/Homeservers/FederationClient.cs b/LibMatrix/Homeservers/FederationClient.cs index dc0d1f6..b7f39eb 100644 --- a/LibMatrix/Homeservers/FederationClient.cs +++ b/LibMatrix/Homeservers/FederationClient.cs @@ -9,7 +9,7 @@ public class FederationClient { public FederationClient(string federationEndpoint, string? proxy = null) { HttpClient = new MatrixHttpClient { BaseAddress = new Uri(proxy?.TrimEnd('/') ?? federationEndpoint.TrimEnd('/')), - Timeout = TimeSpan.FromSeconds(120) + // Timeout = TimeSpan.FromSeconds(120) // TODO: investigate need }; if (proxy is not null) HttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", federationEndpoint); } diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs index 8669ca7..680ade7 100644 --- a/LibMatrix/Homeservers/RemoteHomeServer.cs +++ b/LibMatrix/Homeservers/RemoteHomeServer.cs @@ -19,7 +19,7 @@ public class RemoteHomeserver { WellKnownUris = wellKnownUris; ClientHttpClient = new MatrixHttpClient { BaseAddress = new Uri(proxy?.TrimEnd('/') ?? wellKnownUris.Client?.TrimEnd('/') ?? throw new InvalidOperationException($"No client URI for {baseUrl}!")), - Timeout = TimeSpan.FromSeconds(300) + // Timeout = TimeSpan.FromSeconds(300) // TODO: investigate need }; if (proxy is not null) ClientHttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", baseUrl); diff --git a/LibMatrix/Interfaces/Services/IStorageProvider.cs b/LibMatrix/Interfaces/Services/IStorageProvider.cs deleted file mode 100644 index 165e7df..0000000 --- a/LibMatrix/Interfaces/Services/IStorageProvider.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace LibMatrix.Interfaces.Services; - -public interface IStorageProvider { - // save all children of a type with reflection - public Task SaveAllChildrenAsync(string key, T value) { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveAllChildren(key, value)!"); - throw new NotImplementedException(); - } - - // load all children of a type with reflection - public Task LoadAllChildrenAsync(string key) { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadAllChildren(key)!"); - throw new NotImplementedException(); - } - - public Task SaveObjectAsync(string key, T value) { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveObject(key, value)!"); - throw new NotImplementedException(); - } - - // load - public Task LoadObjectAsync(string key) { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadObject(key)!"); - throw new NotImplementedException(); - } - - // check if exists - public Task ObjectExistsAsync(string key) { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement ObjectExists(key)!"); - throw new NotImplementedException(); - } - - // get all keys - public Task> GetAllKeysAsync() { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement GetAllKeys()!"); - throw new NotImplementedException(); - } - - // delete - public Task DeleteObjectAsync(string key) { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement DeleteObject(key)!"); - throw new NotImplementedException(); - } - - // save stream - public Task SaveStreamAsync(string key, Stream stream) { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveStream(key, stream)!"); - throw new NotImplementedException(); - } - - // load stream - public Task LoadStreamAsync(string key) { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadStream(key)!"); - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj index b85df52..131eea8 100644 --- a/LibMatrix/LibMatrix.csproj +++ b/LibMatrix/LibMatrix.csproj @@ -8,11 +8,13 @@ true true + + true - - + + diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs index f15327c..77bff71 100644 --- a/LibMatrix/RoomTypes/GenericRoom.cs +++ b/LibMatrix/RoomTypes/GenericRoom.cs @@ -106,7 +106,7 @@ public class GenericRoom { Console.WriteLine("WARNING: Homeserver does not support getting event ID from state events, falling back to sync"); var sh = new SyncHelper(Homeserver); var emptyFilter = new SyncFilter.EventFilter(types: [], limit: 1, senders: [], notTypes: ["*"]); - var emptyStateFilter = new SyncFilter.RoomFilter.StateFilter(types: [], limit: 1, senders: [], notTypes: ["*"], rooms:[]); + var emptyStateFilter = new SyncFilter.RoomFilter.StateFilter(types: [], limit: 1, senders: [], notTypes: ["*"], rooms: []); sh.Filter = new() { Presence = emptyFilter, AccountData = emptyFilter, @@ -121,10 +121,11 @@ public class GenericRoom { var sync = await sh.SyncAsync(); var state = sync.Rooms.Join[RoomId].State.Events; var stateEvent = state.FirstOrDefault(x => x.Type == type && x.StateKey == stateKey); - if (stateEvent is null) throw new LibMatrixException() { - ErrorCode = LibMatrixException.ErrorCodes.M_NOT_FOUND, - Error = "State event not found in sync response" - }; + if (stateEvent is null) + throw new LibMatrixException() { + ErrorCode = LibMatrixException.ErrorCodes.M_NOT_FOUND, + Error = "State event not found in sync response" + }; return stateEvent.EventId; } @@ -231,7 +232,7 @@ public class GenericRoom { // var sw = Stopwatch.StartNew(); var res = await Homeserver.ClientHttpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/members"); // if (sw.ElapsedMilliseconds > 1000) - // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}"); // else sw.Restart(); // var resText = await res.Content.ReadAsStringAsync(); // Console.WriteLine($"Members call response read in {sw.GetElapsedAndRestart()}"); @@ -239,7 +240,7 @@ public class GenericRoom { TypeInfoResolver = ChunkedStateEventResponseSerializerContext.Default }); // if (sw.ElapsedMilliseconds > 100) - // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}"); // else sw.Restart(); foreach (var resp in result.Chunk) { if (resp?.Type != "m.room.member") continue; @@ -248,14 +249,14 @@ public class GenericRoom { } // if (sw.ElapsedMilliseconds > 100) - // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}"); } public async Task> GetMembersListAsync(bool joinedOnly = true) { // var sw = Stopwatch.StartNew(); var res = await Homeserver.ClientHttpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/members"); // if (sw.ElapsedMilliseconds > 1000) - // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}"); // else sw.Restart(); // var resText = await res.Content.ReadAsStringAsync(); // Console.WriteLine($"Members call response read in {sw.GetElapsedAndRestart()}"); @@ -263,7 +264,7 @@ public class GenericRoom { TypeInfoResolver = ChunkedStateEventResponseSerializerContext.Default }); // if (sw.ElapsedMilliseconds > 100) - // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}"); // else sw.Restart(); var members = new List(); foreach (var resp in result.Chunk) { @@ -273,7 +274,7 @@ public class GenericRoom { } // if (sw.ElapsedMilliseconds > 100) - // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}"); return members.ToFrozenSet(); } @@ -282,10 +283,10 @@ public class GenericRoom { public Task SendMessageEventAsync(RoomMessageEventContent content) => SendTimelineEventAsync("m.room.message", content); - public async Task?> GetAliasesAsync() { - var res = await GetStateAsync("m.room.aliases"); - return res.Aliases; - } + // public async Task?> GetAliasesAsync() { + // var res = await GetStateAsync(RoomAliasEventContent.EventId); + // return res.Aliases; + // } public Task GetCanonicalAliasAsync() => GetStateAsync("m.room.canonical_alias"); @@ -382,18 +383,18 @@ public class GenericRoom { public async Task KickAsync(string userId, string? reason = null) => await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/kick", - new UserIdAndReason { UserId = userId, Reason = reason }); + new UserIdAndReason(userId, reason)); public async Task BanAsync(string userId, string? reason = null) => await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/ban", - new UserIdAndReason { UserId = userId, Reason = reason }); + new UserIdAndReason(userId, reason)); - public async Task UnbanAsync(string userId) => + public async Task UnbanAsync(string userId, string? reason = null) => await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban", - new UserIdAndReason { UserId = userId }); + new UserIdAndReason(userId, reason)); public async Task InviteUserAsync(string userId, string? reason = null, bool skipExisting = true) { - if (skipExisting && await GetStateAsync("m.room.member", userId) is not null) + if (skipExisting && await GetStateOrNullAsync("m.room.member", userId) is not null) return; await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/invite", new UserIdAndReason(userId, reason)); } @@ -524,21 +525,21 @@ public class GenericRoom { var uri = new Uri(path, UriKind.Relative); if (dir == "b" || dir == "f") uri = uri.AddQuery("dir", dir); - else if(!string.IsNullOrWhiteSpace(dir)) throw new ArgumentException("Invalid direction", nameof(dir)); + else if (!string.IsNullOrWhiteSpace(dir)) throw new ArgumentException("Invalid direction", nameof(dir)); if (!string.IsNullOrEmpty(from)) uri = uri.AddQuery("from", from); if (chunkLimit is not null) uri = uri.AddQuery("limit", chunkLimit.Value.ToString()); if (recurse is not null) uri = uri.AddQuery("recurse", recurse.Value.ToString()); if (!string.IsNullOrEmpty(to)) uri = uri.AddQuery("to", to); // Console.WriteLine($"Getting related events from {uri}"); - var result = await Homeserver.ClientHttpClient.GetFromJsonAsync(uri); + var result = await Homeserver.ClientHttpClient.GetFromJsonAsync(uri.ToString()); //TODO: investigate ToString call while (result!.Chunk.Count > 0) { foreach (var resp in result.Chunk) { yield return resp; } if (result.NextBatch is null) break; - result = await Homeserver.ClientHttpClient.GetFromJsonAsync(uri.AddQuery("from", result.NextBatch)); + result = await Homeserver.ClientHttpClient.GetFromJsonAsync(uri.AddQuery("from", result.NextBatch).ToString()); //TODO: investigate ToString call } } diff --git a/LibMatrix/RoomTypes/SpaceRoom.cs b/LibMatrix/RoomTypes/SpaceRoom.cs index b40ccc6..45069d9 100644 --- a/LibMatrix/RoomTypes/SpaceRoom.cs +++ b/LibMatrix/RoomTypes/SpaceRoom.cs @@ -1,9 +1,12 @@ +using System.Text.Json.Nodes; using ArcaneLibs.Extensions; using LibMatrix.Homeservers; namespace LibMatrix.RoomTypes; public class SpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomId) : GenericRoom(homeserver, roomId) { + public const string TypeName = "m.space"; + public async IAsyncEnumerable GetChildrenAsync(bool includeRemoved = false) { // var rooms = new List(); var state = GetFullStateAsync(); @@ -31,7 +34,7 @@ public class SpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomId) }); return resp; } - + public async Task AddChildByIdAsync(string id) { return await AddChildAsync(Homeserver.GetRoom(id)); } diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs index 05ce733..27ad594 100644 --- a/LibMatrix/Services/HomeserverResolverService.cs +++ b/LibMatrix/Services/HomeserverResolverService.cs @@ -14,7 +14,7 @@ namespace LibMatrix.Services; public class HomeserverResolverService { private readonly MatrixHttpClient _httpClient = new() { - Timeout = TimeSpan.FromSeconds(60) + // Timeout = TimeSpan.FromSeconds(60) //TODO: investigate need }; private static readonly SemaphoreCache WellKnownCache = new(); diff --git a/LibMatrix/Services/TieredStorageService.cs b/LibMatrix/Services/TieredStorageService.cs deleted file mode 100644 index 9e411de..0000000 --- a/LibMatrix/Services/TieredStorageService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using LibMatrix.Interfaces.Services; - -namespace LibMatrix.Services; - -public class TieredStorageService(IStorageProvider? cacheStorageProvider, IStorageProvider? dataStorageProvider) { - public IStorageProvider? CacheStorageProvider { get; } = cacheStorageProvider; - public IStorageProvider? DataStorageProvider { get; } = dataStorageProvider; -} \ No newline at end of file diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs index 81ee3fe..3bd0672 100644 --- a/LibMatrix/StateEvent.cs +++ b/LibMatrix/StateEvent.cs @@ -1,6 +1,7 @@ using System.Collections.Frozen; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -54,6 +55,8 @@ public class StateEvent { [JsonIgnore] [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] public EventContent? TypedContent { + [RequiresDynamicCode("TypedContent requires reflection to deserialize the content of the event.")] + [RequiresUnreferencedCode("TypedContent requires reflection to deserialize the content of the event.")] get { // if (Type == "m.receipt") { // return null; @@ -72,6 +75,9 @@ public class StateEvent { return null; } + + [RequiresDynamicCode("TypedContent requires reflection to deserialize the content of the event.")] + [RequiresUnreferencedCode("TypedContent requires reflection to deserialize the content of the event.")] set { if (value is null) RawContent?.Clear(); diff --git a/LibMatrix/UserIdAndReason.cs b/LibMatrix/UserIdAndReason.cs index 99c9eaf..5e52303 100644 --- a/LibMatrix/UserIdAndReason.cs +++ b/LibMatrix/UserIdAndReason.cs @@ -2,7 +2,7 @@ using System.Text.Json.Serialization; namespace LibMatrix; -internal class UserIdAndReason(string userId = null!, string reason = null!) { +internal class UserIdAndReason(string userId = null!, string? reason = null) { [JsonPropertyName("user_id")] public string UserId { get; set; } = userId; diff --git a/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj b/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj index 0a43299..122cfba 100644 --- a/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj +++ b/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj @@ -10,9 +10,8 @@ - - - + + diff --git a/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs index c9727d6..401223c 100644 --- a/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs +++ b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs @@ -1,71 +1,96 @@ using ArcaneLibs.Extensions; using LibMatrix.Homeservers; using LibMatrix.Responses; +using LibMatrix.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit.Abstractions; +using Xunit.Sdk; namespace LibMatrix.Tests.Abstractions; -public static class HomeserverAbstraction { - public static async Task GetHomeserver() { - var rhs = await RemoteHomeserver.Create("https://matrixunittests.rory.gay"); - // string username = Guid.NewGuid().ToString(); - // string password = Guid.NewGuid().ToString(); - var username = "@f1a2d2d6-1924-421b-91d0-893b347b2a49:matrixunittests.rory.gay"; - var password = "d6d782d6-8bc9-4fac-9cd8-78e101b4298b"; +public class HomeserverAbstraction(HomeserverProviderService _hsProvider, Config _config, ILogger _logger) { + // private static readonly HomeserverResolverService _hsResolver = new HomeserverResolverService(NullLogger.Instance); + // private static readonly HomeserverProviderService _hsProvider = new HomeserverProviderService(NullLogger.Instance, _hsResolver); + + private static AuthenticatedHomeserverGeneric? ConfiguredHomeserver { get; set; } + private static readonly SemaphoreSlim _lock = new(1, 1); + + public async Task GetConfiguredHomeserver(ITestOutputHelper? testOutputHelper = null) { + Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver)); + Assert.False(string.IsNullOrWhiteSpace(_config.TestUsername)); + Assert.False(string.IsNullOrWhiteSpace(_config.TestPassword)); + + _logger.LogDebug("Using homeserver '{0}' with login '{1}' '{2}", _config.TestHomeserver, _config.TestUsername, _config.TestPassword); + testOutputHelper?.WriteLine($"Using homeserver '{_config.TestHomeserver}' with login '{_config.TestUsername}' '{_config.TestPassword}'"); + + await _lock.WaitAsync(); + if (ConfiguredHomeserver is not null) { + _lock.Release(); + return ConfiguredHomeserver; + } + + var rhs = await _hsProvider.GetRemoteHomeserver(_config.TestHomeserver); + LoginResponse reg; try { - reg = await rhs.LoginAsync(username, password); + reg = await rhs.LoginAsync(_config.TestUsername, _config.TestPassword); } 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!"); + reg = await rhs.RegisterAsync(_config.TestUsername, _config.TestPassword, "Unit tests!"); } else throw new Exception("Failed to register", e); } - var hs = await reg.GetAuthenticatedHomeserver("https://matrixunittests.rory.gay"); - - //var rooms = await hs.GetJoinedRooms(); - - // var disbandRoomTasks = rooms.Select(async room => { - // // await room.DisbandRoomAsync(); - // await room.LeaveAsync(); - // await room.ForgetAsync(); - // return room; - // }).ToList(); - // await Task.WhenAll(disbandRoomTasks); - - // foreach (var room in rooms) { - // // await room.DisbandRoomAsync(); - // await room.LeaveAsync(); - // await room.ForgetAsync(); - // } + var hs = await _hsProvider.GetAuthenticatedWithToken(reg.Homeserver, reg.AccessToken); + ConfiguredHomeserver = hs; + _lock.Release(); return hs; } - public static async Task GetRandomHomeserver() { - var rhs = await RemoteHomeserver.Create("https://matrixunittests.rory.gay"); - var reg = await rhs.RegisterAsync(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "Unit tests!"); - var hs = await reg.GetAuthenticatedHomeserver("https://matrixunittests.rory.gay"); - - // var rooms = await hs.GetJoinedRooms(); - // - // var disbandRoomTasks = rooms.Select(async room => { - // // await room.DisbandRoomAsync(); - // await room.LeaveAsync(); - // await room.ForgetAsync(); - // return room; - // }).ToList(); - // await Task.WhenAll(disbandRoomTasks); - + public async Task GetNewHomeserver() { + Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver)); + var username = Guid.NewGuid().ToString(); + var password = Guid.NewGuid().ToString(); + + _logger.LogDebug("Creating new homeserver '{0}' with login '{1}' '{2}'", _config.TestHomeserver, username, password); + + var rhs = await _hsProvider.GetRemoteHomeserver(_config.TestHomeserver); + var reg = await rhs.RegisterAsync(username, password, "Unit tests!"); + var hs = await _hsProvider.GetAuthenticatedWithToken(reg.Homeserver, reg.AccessToken); + return hs; } - public static async IAsyncEnumerable GetRandomHomeservers(int count = 1) { + public async IAsyncEnumerable GetNewHomeservers(int count = 1) { var createRandomUserTasks = Enumerable .Range(0, count) - .Select(_ => GetRandomHomeserver()).ToAsyncEnumerable(); + .Select(_ => GetNewHomeserver()).ToAsyncEnumerable(); await foreach (var hs in createRandomUserTasks) yield return hs; } + + public async Task<(string username, string password, string token)> GetKnownCredentials() { + Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver)); + var rhs = await _hsProvider.GetRemoteHomeserver(_config.TestHomeserver); + + 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); + } } \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs b/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs index 2a380fc..88b6758 100644 --- a/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs +++ b/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using ArcaneLibs.Extensions; using LibMatrix.EventTypes.Spec.State; using LibMatrix.EventTypes.Spec.State.RoomInfo; @@ -16,28 +17,28 @@ public static class RoomAbstraction { }; crq.InitialState ??= new List(); crq.InitialState.Add(new StateEvent() { - Type = "m.room.topic", + Type = RoomTopicEventContent.EventId, StateKey = "", TypedContent = new RoomTopicEventContent() { Topic = "LibMatrix Test Room " + DateTime.Now.ToString("O") } }); crq.InitialState.Add(new StateEvent() { - Type = "m.room.name", + Type = RoomNameEventContent.EventId, StateKey = "", TypedContent = new RoomNameEventContent() { Name = "LibMatrix Test Room " + DateTime.Now.ToString("O") } }); crq.InitialState.Add(new StateEvent() { - Type = "m.room.avatar", + Type = RoomAvatarEventContent.EventId, StateKey = "", TypedContent = new RoomAvatarEventContent() { Url = "mxc://conduit.rory.gay/r9KiT0f9eQbv8pv4RxwBZFuzhfKjGWHx" } }); crq.InitialState.Add(new StateEvent() { - Type = "m.room.aliases", + Type = RoomAliasEventContent.EventId, StateKey = "", TypedContent = new RoomAliasEventContent() { Aliases = Enumerable diff --git a/Tests/LibMatrix.Tests/Config.cs b/Tests/LibMatrix.Tests/Config.cs index ddbf705..045ea40 100644 --- a/Tests/LibMatrix.Tests/Config.cs +++ b/Tests/LibMatrix.Tests/Config.cs @@ -1,18 +1,41 @@ +using Microsoft.Extensions.Configuration; + namespace LibMatrix.Tests; public class Config { + public Config(IConfiguration? config) { + config.GetSection("Configuration").Bind(this); + } + public string? TestHomeserver { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_HOMESERVER") ?? null; - public string? TestUsername { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_USERNAME") ?? null; - public string? TestPassword { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_PASSWORD") ?? null; - public string? TestRoomId { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_ROOM_ID") ?? null; - public string? TestRoomAlias { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_ROOM_ALIAS") ?? null; + public string? TestUsername { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_USERNAME") ?? Guid.NewGuid().ToString(); + + public string? TestPassword { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_PASSWORD") ?? Guid.NewGuid().ToString(); + // public string? TestRoomId { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_ROOM_ID") ?? null; + // public string? TestRoomAlias { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_ROOM_ALIAS") ?? null; - public Dictionary ExpectedHomeserverMappings { get; set; } = new() { + public Dictionary ExpectedHomeserverClientMappings { get; set; } = new() { { "matrix.org", "https://matrix-client.matrix.org" }, - { "rory.gay", "https://matrix.rory.gay" } + { "rory.gay", "https://matrix.rory.gay" }, + { "feline.support", "https://matrix.feline.support" }, + { "transfem.dev", "https://matrix.transfem.dev" }, + { "the-apothecary.club", "https://the-apothecary.club" }, + { "nixos.org", "https://matrix.nixos.org" }, + { "fedora.im", "https://fedora.ems.host" } + }; + + public Dictionary ExpectedHomeserverFederationMappings { get; set; } = new() { + { "rory.gay", "https://matrix.rory.gay:443" }, + { "matrix.org", "https://matrix-federation.matrix.org:443" }, + { "feline.support", "https://matrix.feline.support:8448" }, + { "transfem.dev", "https://matrix.transfem.dev:443" }, + { "the-apothecary.club", "https://the-apothecary.club:443" }, + { "nixos.org", "https://matrix.nixos.org:443" }, + { "fedora.im", "https://fedora.ems.host:443" } }; public Dictionary ExpectedAliasMappings { get; set; } = new() { - { "#libmatrix:rory.gay", "!tuiLEoMqNOQezxILzt:rory.gay" } + { "#libmatrix:rory.gay", "!tuiLEoMqNOQezxILzt:rory.gay" }, + { "#matrix:matrix.org", "!OGEhHVWSdvArJzumhm:matrix.org" } }; } \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Fixtures/TestFixture.cs b/Tests/LibMatrix.Tests/Fixtures/TestFixture.cs index 35c8704..01a0d2f 100644 --- a/Tests/LibMatrix.Tests/Fixtures/TestFixture.cs +++ b/Tests/LibMatrix.Tests/Fixtures/TestFixture.cs @@ -1,5 +1,6 @@ using ArcaneLibs.Extensions; using LibMatrix.Services; +using LibMatrix.Tests.Abstractions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Xunit.Microsoft.DependencyInjection; @@ -8,24 +9,19 @@ using Xunit.Microsoft.DependencyInjection.Abstracts; namespace LibMatrix.Tests.Fixtures; public class TestFixture : TestBedFixture { - protected override void AddServices(IServiceCollection services, IConfiguration? configuration) { - services.AddSingleton(x => - new TieredStorageService( - null, - null - ) - ); + protected override void AddServices(IServiceCollection services, IConfiguration configuration) { + // services.AddSingleton(x => + // new TieredStorageService( + // null, + // null + // ) + // ); + services.AddSingleton(configuration); services.AddRoryLibMatrixServices(); - - services.AddSingleton(config => { - var conf = new Config(); - configuration?.GetSection("Configuration").Bind(conf); - - File.WriteAllText("configuration.json", conf.ToJson()); - - return conf; - }); + services.AddLogging(); + services.AddSingleton(); + services.AddSingleton(); } protected override ValueTask DisposeAsyncCore() diff --git a/Tests/LibMatrix.Tests/LibMatrix.Tests.csproj b/Tests/LibMatrix.Tests/LibMatrix.Tests.csproj index d833d8b..52bec9f 100644 --- a/Tests/LibMatrix.Tests/LibMatrix.Tests.csproj +++ b/Tests/LibMatrix.Tests/LibMatrix.Tests.csproj @@ -12,14 +12,14 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Tests/LibMatrix.Tests/Tests/AuthTests.cs b/Tests/LibMatrix.Tests/Tests/AuthTests.cs index 67ba8eb..c1fefe4 100644 --- a/Tests/LibMatrix.Tests/Tests/AuthTests.cs +++ b/Tests/LibMatrix.Tests/Tests/AuthTests.cs @@ -1,4 +1,5 @@ using LibMatrix.Services; +using LibMatrix.Tests.Abstractions; using LibMatrix.Tests.DataTests; using LibMatrix.Tests.Fixtures; using Xunit.Abstractions; @@ -7,43 +8,29 @@ using Xunit.Microsoft.DependencyInjection.Abstracts; namespace LibMatrix.Tests.Tests; public class AuthTests : TestBed { - private readonly TestFixture _fixture; - private readonly HomeserverResolverService _resolver; private readonly Config _config; private readonly HomeserverProviderService _provider; + private readonly HomeserverAbstraction _hsAbstraction; public AuthTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { - _fixture = fixture; - _resolver = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); _config = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); _provider = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}"); + _hsAbstraction = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); } - + [Fact] public async Task LoginWithPassword() { - 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 login = await _provider.Login(_config.TestHomeserver!, _config.TestUsername!, _config.TestPassword!); + var credentials = await _hsAbstraction.GetKnownCredentials(); + + var login = await _provider.Login(_config.TestHomeserver!, credentials.username, credentials.password); Assert.NotNull(login); - var hs = await _provider.GetAuthenticatedWithToken(_config.TestHomeserver!, login.AccessToken); - Assert.NotNull(hs); - await hs.Logout(); + Assert.NotNull(login.AccessToken); } [Fact] public async Task LoginWithToken() { - 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 login = await _provider.Login(_config.TestHomeserver!, _config.TestUsername!, _config.TestPassword!); - Assert.NotNull(login); - - var hs = await _provider.GetAuthenticatedWithToken(_config.TestHomeserver!, login.AccessToken); + var credentials = await _hsAbstraction.GetKnownCredentials(); + var hs = await _provider.GetAuthenticatedWithToken(_config.TestHomeserver!, credentials.token); Assert.NotNull(hs); Assert.NotNull(hs.WhoAmI); hs.WhoAmI.VerifyRequiredFields(); @@ -60,7 +47,7 @@ public class AuthTests : TestBed { Assert.NotNull(reg.AccessToken); Assert.NotNull(reg.DeviceId); Assert.NotNull(reg.UserId); - var hs = await reg.GetAuthenticatedHomeserver(); + var hs = await _provider.GetAuthenticatedWithToken(reg.Homeserver, reg.AccessToken); Assert.NotNull(hs); Assert.NotNull(hs.WhoAmI); hs.WhoAmI.VerifyRequiredFields(); diff --git a/Tests/LibMatrix.Tests/Tests/HomeserverResolverTests.cs b/Tests/LibMatrix.Tests/Tests/HomeserverResolverTests.cs new file mode 100644 index 0000000..ef2426d --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/HomeserverResolverTests.cs @@ -0,0 +1,43 @@ +using LibMatrix.Services; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests; + +public class HomeserverResolverTests : TestBed { + private readonly Config _config; + private readonly HomeserverResolverService _resolver; + + public HomeserverResolverTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _config = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); + _resolver = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); + } + + [Fact] + public async Task ResolveServerClient() { + var tasks = _config.ExpectedHomeserverClientMappings.Select(async mapping => { + var server = await _resolver.ResolveHomeserverFromWellKnown(mapping.Key); + Assert.Equal(mapping.Value, server.Client); + return server; + }).ToList(); + await Task.WhenAll(tasks); + } + + [Fact] + public async Task ResolveServerServer() { + var tasks = _config.ExpectedHomeserverFederationMappings.Select(async mapping => { + var server = await _resolver.ResolveHomeserverFromWellKnown(mapping.Key); + Assert.Equal(mapping.Value, server.Server); + return server; + }).ToList(); + await Task.WhenAll(tasks); + } + + [Fact] + public async Task ResolveMedia() { + var media = await _resolver.ResolveMediaUri("matrix.org", "mxc://matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo"); + + Assert.Equal("https://matrix-client.matrix.org/_matrix/media/v3/download/matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo", media); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs b/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs new file mode 100644 index 0000000..03f3c24 --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs @@ -0,0 +1,56 @@ +using LibMatrix.Services; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests; + +public class RemoteHomeserverTests : TestBed { + private readonly TestFixture _fixture; + private readonly HomeserverResolverService _resolver; + private readonly Config _config; + private readonly HomeserverProviderService _provider; + + public RemoteHomeserverTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _fixture = fixture; + _resolver = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); + _config = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); + _provider = _fixture.GetService(_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 ResolveRoomAliasAsync() { + // var hs = await _provider.GetRemoteHomeserver("matrix.org"); + // var alias = await hs.ResolveRoomAliasAsync("#matrix:matrix.org"); + // Assert.Equal("!OGEhHVWSdvArJzumhm:matrix.org", alias.RoomId); + var tasks = _config.ExpectedAliasMappings.Select(async mapping => { + var hs = await _provider.GetRemoteHomeserver("matrix.org"); + var alias = await hs.ResolveRoomAliasAsync(mapping.Key); + Assert.Equal(mapping.Value, alias.RoomId); + return alias; + }).ToList(); + await Task.WhenAll(tasks); + } + + [Fact] + public async Task GetClientVersionsAsync() { + var hs = await _provider.GetRemoteHomeserver(_config.TestHomeserver); + var versions = await hs.GetClientVersionsAsync(); + Assert.NotNull(versions); + } + + [Fact] + public async Task GetProfileAsync() { + var hs = await _provider.GetRemoteHomeserver("matrix.org"); + var profile = await hs.GetProfileAsync("@alice-is-:matrix.org"); + Assert.NotNull(profile); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/ResolverTest.cs b/Tests/LibMatrix.Tests/Tests/ResolverTest.cs deleted file mode 100644 index 700aa96..0000000 --- a/Tests/LibMatrix.Tests/Tests/ResolverTest.cs +++ /dev/null @@ -1,55 +0,0 @@ -using LibMatrix.Services; -using LibMatrix.Tests.Fixtures; -using Xunit.Abstractions; -using Xunit.Microsoft.DependencyInjection.Abstracts; - -namespace LibMatrix.Tests.Tests; - -public class ResolverTest : TestBed { - private readonly TestFixture _fixture; - private readonly HomeserverResolverService _resolver; - private readonly Config _config; - private readonly HomeserverProviderService _provider; - - public ResolverTest(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { - _fixture = fixture; - _resolver = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); - _config = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); - _provider = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}"); - } - - [Fact] - public async Task ResolveServer() { - foreach (var (domain, expected) in _config.ExpectedHomeserverMappings) { - var server = await _resolver.ResolveHomeserverFromWellKnown(domain); - Assert.Equal(expected, server.Client); - } - } - - [Fact] - public async Task ResolveMedia() { - var media = await _resolver.ResolveMediaUri("matrix.org", "mxc://matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo"); - Assert.Equal("https://matrix-client.matrix.org/_matrix/media/v3/download/matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo", media); - } - - [Fact] - public async Task ResolveRoomAliasAsync() { - var hs = await _provider.GetRemoteHomeserver("matrix.org"); - var alias = await hs.ResolveRoomAliasAsync("#matrix:matrix.org"); - Assert.Equal("!OGEhHVWSdvArJzumhm:matrix.org", alias.RoomId); - } - - [Fact] - public async Task GetClientVersionsAsync() { - var hs = await _provider.GetRemoteHomeserver("matrix.org"); - var versions = await hs.GetClientVersionsAsync(); - Assert.NotNull(versions); - } - - [Fact] - public async Task GetProfileAsync() { - var hs = await _provider.GetRemoteHomeserver("matrix.org"); - var profile = await hs.GetProfileAsync("@alice-is-:matrix.org"); - Assert.NotNull(profile); - } -} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomEventTests.cs b/Tests/LibMatrix.Tests/Tests/RoomEventTests.cs deleted file mode 100644 index 932909f..0000000 --- a/Tests/LibMatrix.Tests/Tests/RoomEventTests.cs +++ /dev/null @@ -1,154 +0,0 @@ -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 RoomEventTests : TestBed { - private readonly TestFixture _fixture; - private readonly HomeserverResolverService _resolver; - private readonly Config _config; - private readonly HomeserverProviderService _provider; - - public RoomEventTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { - _fixture = fixture; - _resolver = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); - _config = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); - _provider = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}"); - } - - private async Task GetHomeserver() => await HomeserverAbstraction.GetHomeserver(); - - [Fact] - public async Task GetNameAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var name = await room.GetNameAsync(); - Assert.NotNull(name); - Assert.NotEmpty(name); - } - - [SkippableFact(typeof(MatrixException))] - public async Task GetTopicAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var topic = await room.GetTopicAsync(); - Assert.NotNull(topic); - Assert.NotNull(topic.Topic); - Assert.NotEmpty(topic.Topic); - } - - [SkippableFact(typeof(MatrixException))] - public async Task GetAliasesAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var aliases = await room.GetAliasesAsync(); - Assert.NotNull(aliases); - Assert.NotEmpty(aliases); - Assert.All(aliases, Assert.NotNull); - } - - [SkippableFact(typeof(MatrixException))] - public async Task GetCanonicalAliasAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var alias = await room.GetCanonicalAliasAsync(); - Assert.NotNull(alias); - Assert.NotNull(alias.Alias); - Assert.NotEmpty(alias.Alias); - } - - [SkippableFact(typeof(MatrixException))] - public async Task GetAvatarUrlAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var url = await room.GetAvatarUrlAsync(); - Assert.NotNull(url); - Assert.NotNull(url.Url); - Assert.NotEmpty(url.Url); - } - - [Fact] - public async Task GetJoinRuleAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var rule = await room.GetJoinRuleAsync(); - Assert.NotNull(rule); - Assert.NotNull(rule.JoinRuleValue); - Assert.NotEmpty(rule.JoinRuleValue); - } - - [Fact] - public async Task GetHistoryVisibilityAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var visibility = await room.GetHistoryVisibilityAsync(); - Assert.NotNull(visibility); - Assert.NotNull(visibility.HistoryVisibility); - Assert.NotEmpty(visibility.HistoryVisibility); - } - - [Fact] - public async Task GetGuestAccessAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - try { - var access = await room.GetGuestAccessAsync(); - Assert.NotNull(access); - Assert.NotNull(access.GuestAccess); - Assert.NotEmpty(access.GuestAccess); - } - catch (Exception e) { - if (e is not MatrixException exception) throw; - Assert.Equal("M_NOT_FOUND", exception.ErrorCode); - } - } - - [Fact] - public async Task GetCreateEventAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var create = await room.GetCreateEventAsync(); - Assert.NotNull(create); - Assert.NotNull(create.Creator); - Assert.NotEmpty(create.RoomVersion!); - } - - [Fact] - public async Task GetRoomType() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - await room.GetRoomType(); - } - - [Fact] - public async Task GetPowerLevelsAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var power = await room.GetPowerLevelsAsync(); - Assert.NotNull(power); - Assert.NotNull(power.Ban); - Assert.NotNull(power.Kick); - Assert.NotNull(power.Invite); - Assert.NotNull(power.Redact); - Assert.NotNull(power.StateDefault); - Assert.NotNull(power.EventsDefault); - Assert.NotNull(power.UsersDefault); - Assert.NotNull(power.Users); - // Assert.NotNull(power.Events); - } -} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests.cs deleted file mode 100644 index 4c8dcb4..0000000 --- a/Tests/LibMatrix.Tests/Tests/RoomTests.cs +++ /dev/null @@ -1,248 +0,0 @@ -using System.Text; -using LibMatrix.EventTypes.Spec.State; -using LibMatrix.Homeservers; -using LibMatrix.Responses; -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 RoomTests : TestBed { - private readonly TestFixture _fixture; - private readonly HomeserverResolverService _resolver; - private readonly Config _config; - private readonly HomeserverProviderService _provider; - - public RoomTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { - _fixture = fixture; - _resolver = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); - _config = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); - _provider = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}"); - } - - private async Task GetHomeserver() => await HomeserverAbstraction.GetHomeserver(); - - [Fact] - public async Task GetJoinedRoomsAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - //make 100 rooms - var createRoomTasks = Enumerable.Range(0, 10).Select(_ => RoomAbstraction.GetTestRoom(hs)).ToList(); - await Task.WhenAll(createRoomTasks); - - var rooms = await hs.GetJoinedRooms(); - Assert.NotNull(rooms); - Assert.NotEmpty(rooms); - Assert.All(rooms, Assert.NotNull); - Assert.True(rooms.Count >= 10, "Not enough rooms were found"); - - await hs.Logout(); - } - - [Fact] - public async Task GetMembersAsync() { - Assert.True(StateEvent.KnownStateEventTypes is { Count: > 0 }, "StateEvent.KnownStateEventTypes is empty!"); - Assert.True(StateEvent.KnownStateEventTypesByName is { Count: > 0 }, "StateEvent.KnownStateEventTypesByName is empty!"); - - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var members = room.GetMembersEnumerableAsync(); - Assert.NotNull(members); - var hitMembers = false; - await foreach (var member in members) { - Assert.NotNull(member); - Assert.NotNull(member.StateKey); - Assert.NotEmpty(member.StateKey); - Assert.NotNull(member.Sender); - Assert.NotEmpty(member.Sender); - Assert.NotNull(member.RawContent); - Assert.NotEmpty(member.RawContent); - Assert.NotNull(member.TypedContent); - Assert.IsType(member.TypedContent); - var content = (RoomMemberEventContent)member.TypedContent; - Assert.NotNull(content); - Assert.NotNull(content.Membership); - Assert.NotEmpty(content.Membership); - hitMembers = true; - } - - Assert.True(hitMembers, "No members were found in the room"); - } - - [Fact] - public async Task JoinAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var id = await room.JoinAsync(); - Assert.NotNull(id); - Assert.NotNull(id.RoomId); - Assert.NotEmpty(id.RoomId); - } - - [Fact] - public async Task ForgetAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - await room.ForgetAsync(); - } - - [Fact] - public async Task LeaveAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - await room.LeaveAsync(); - } - - [Fact] - public async Task KickAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var hs2 = await HomeserverAbstraction.GetRandomHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - await room.InviteUserAsync(hs2.UserId, "Unit test!"); - await hs2.GetRoom(room.RoomId).JoinAsync(); - await room.KickAsync(hs2.UserId, "test"); - var banState = await room.GetStateAsync("m.room.member", hs2.UserId); - Assert.NotNull(banState); - Assert.Equal("leave", banState.Membership); - } - - [Fact] - public async Task BanAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var hs2 = await HomeserverAbstraction.GetRandomHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - await room.BanAsync(hs2.UserId, "test"); - var banState = await room.GetStateAsync("m.room.member", hs2.UserId); - Assert.NotNull(banState); - Assert.Equal("ban", banState.Membership); - } - - [Fact] - public async Task UnbanAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var hs2 = await HomeserverAbstraction.GetRandomHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - await room.BanAsync(hs2.UserId, "test"); - var banState = await room.GetStateAsync("m.room.member", hs2.UserId); - Assert.NotNull(banState); - Assert.Equal("ban", banState.Membership); - await room.UnbanAsync(hs2.UserId); - var unbanState = await room.GetStateAsync("m.room.member", hs2.UserId); - Assert.NotNull(unbanState); - Assert.Equal("leave", unbanState.Membership); - } - - [SkippableFact(typeof(MatrixException))] - public async Task SendStateEventAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - - await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", new UserProfileResponse() { - DisplayName = "wee_woo", - AvatarUrl = "no" - }); - await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", "state_key_maybe", new UserProfileResponse() { - DisplayName = "wee_woo", - AvatarUrl = "yes" - }); - } - - [SkippableFact(typeof(MatrixException))] - public async Task SendAndGetStateEventAsync() { - await SendStateEventAsync(); - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - - await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", new UserProfileResponse() { - DisplayName = "wee_woo", - AvatarUrl = "no" - }); - await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", "state_key_maybe", new UserProfileResponse() { - DisplayName = "wee_woo", - AvatarUrl = "yes" - }); - - var state1 = await room.GetStateAsync("gay.rory.libmatrix.unit_tests"); - Assert.NotNull(state1); - Assert.NotNull(state1.DisplayName); - Assert.NotEmpty(state1.DisplayName); - Assert.NotNull(state1.AvatarUrl); - Assert.NotEmpty(state1.AvatarUrl); - Assert.Equal("wee_woo", state1.DisplayName); - Assert.Equal("no", state1.AvatarUrl); - - var state2 = await room.GetStateAsync("gay.rory.libmatrix.unit_tests", "state_key_maybe"); - Assert.NotNull(state2); - Assert.NotNull(state2.DisplayName); - Assert.NotEmpty(state2.DisplayName); - Assert.NotNull(state2.AvatarUrl); - Assert.NotEmpty(state2.AvatarUrl); - Assert.Equal("wee_woo", state2.DisplayName); - Assert.Equal("yes", state2.AvatarUrl); - } - - [Fact] - public async Task DisbandAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - - await room.PermanentlyBrickRoomAsync(); - } - - [Fact] - public async Task SendFileAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - - var res = await room.SendFileAsync("test.txt", new MemoryStream(Encoding.UTF8.GetBytes("This test was written by Emma [it/its], member of the Rory& system." + - "\nIf you are reading this on matrix, it means the unit test for uploading a file works!"))); - Assert.NotNull(res); - Assert.NotNull(res.EventId); - } - - [Fact] - public async Task GetSpaceChildrenAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var space = await RoomAbstraction.GetTestSpace(hs, 2, false, 1); - Assert.NotNull(space); - var children = space.GetChildrenAsync(); - Assert.NotNull(children); - var found = 0; - await foreach (var room in children) found++; - Assert.Equal(2, found); - } - - [Fact] - public async Task InviteAndJoinAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - var otherUsers = HomeserverAbstraction.GetRandomHomeservers(15); - Assert.NotNull(room); - - // var expectedCount = 1; - - var tasks = new List(); - await foreach (var otherUser in otherUsers) - tasks.Add(Task.Run(async () => { - await room.InviteUserAsync(otherUser.UserId); - await otherUser.GetRoom(room.RoomId).JoinAsync(); - })); - await Task.WhenAll(tasks); - - var states = await room.GetMembersListAsync(false); - Assert.Equal(16, states.Count); - } -} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/OtherRoomTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/OtherRoomTests.cs new file mode 100644 index 0000000..1ae195d --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/OtherRoomTests.cs @@ -0,0 +1,101 @@ +using LibMatrix.Tests.Abstractions; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests.BasicRoomEventTests; + +public class OtherRoomTests : TestBed { + private readonly HomeserverAbstraction _hsAbstraction; + + public OtherRoomTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _hsAbstraction = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); + } + + [SkippableFact(typeof(MatrixException))] + public async Task GetCanonicalAliasAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var alias = await room.GetCanonicalAliasAsync(); + Assert.NotNull(alias); + Assert.NotNull(alias.Alias); + Assert.NotEmpty(alias.Alias); + } + + [Fact] + public async Task GetJoinRuleAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var rule = await room.GetJoinRuleAsync(); + Assert.NotNull(rule); + Assert.NotNull(rule.JoinRuleValue); + Assert.NotEmpty(rule.JoinRuleValue); + } + + [Fact] + public async Task GetHistoryVisibilityAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var visibility = await room.GetHistoryVisibilityAsync(); + Assert.NotNull(visibility); + Assert.NotNull(visibility.HistoryVisibility); + Assert.NotEmpty(visibility.HistoryVisibility); + } + + [Fact] + public async Task GetGuestAccessAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + try { + var access = await room.GetGuestAccessAsync(); + Assert.NotNull(access); + Assert.NotNull(access.GuestAccess); + Assert.NotEmpty(access.GuestAccess); + } + catch (Exception e) { + if (e is not MatrixException exception) throw; + Assert.Equal("M_NOT_FOUND", exception.ErrorCode); + } + } + + [Fact] + public async Task GetCreateEventAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var create = await room.GetCreateEventAsync(); + Assert.NotNull(create); + Assert.NotNull(create.Creator); + Assert.NotEmpty(create.RoomVersion!); + } + + [Fact] + public async Task GetRoomType() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + await room.GetRoomType(); + } + + [Fact] + public async Task GetPowerLevelsAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var power = await room.GetPowerLevelsAsync(); + Assert.NotNull(power); + Assert.NotNull(power.Ban); + Assert.NotNull(power.Kick); + Assert.NotNull(power.Invite); + Assert.NotNull(power.Redact); + Assert.NotNull(power.StateDefault); + Assert.NotNull(power.EventsDefault); + Assert.NotNull(power.UsersDefault); + Assert.NotNull(power.Users); + // Assert.NotNull(power.Events); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomAvatarTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomAvatarTests.cs new file mode 100644 index 0000000..78f007c --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomAvatarTests.cs @@ -0,0 +1,27 @@ +using LibMatrix.Tests.Abstractions; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests.BasicRoomEventTests; + +public class RoomAvatarTests : TestBed { + private readonly HomeserverAbstraction _hsAbstraction; + + public RoomAvatarTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _hsAbstraction = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); + } + + [SkippableFact(typeof(MatrixException))] + public async Task GetAvatarUrlAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var url = await room.GetAvatarUrlAsync(); + Assert.NotNull(url); + Assert.NotNull(url.Url); + Assert.NotEmpty(url.Url); + + await room.LeaveAsync(); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomNameTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomNameTests.cs new file mode 100644 index 0000000..1ea3e18 --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomNameTests.cs @@ -0,0 +1,42 @@ +using LibMatrix.EventTypes.Spec.State; +using LibMatrix.Tests.Abstractions; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests.BasicRoomEventTests; + +public class RoomNameTests : TestBed { + private readonly HomeserverAbstraction _hsAbstraction; + + public RoomNameTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _hsAbstraction = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); + } + + [Fact] + public async Task GetNameAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var name = await room.GetNameAsync(); + Assert.NotNull(name); + Assert.NotEmpty(name); + + await room.LeaveAsync(); + } + + [Fact] + public async Task SetNameAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var name = Guid.NewGuid().ToString(); + await room.SendStateEventAsync(RoomNameEventContent.EventId, new RoomNameEventContent { Name = name }); + var newName = await room.GetNameAsync(); + Assert.Equal(name, newName); + + await room.LeaveAsync(); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomTopicTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomTopicTests.cs new file mode 100644 index 0000000..6610035 --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomTopicTests.cs @@ -0,0 +1,27 @@ +using LibMatrix.Tests.Abstractions; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests.BasicRoomEventTests; + +public class RoomTopicTests : TestBed { + private readonly HomeserverAbstraction _hsAbstraction; + + public RoomTopicTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _hsAbstraction = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); + } + + [SkippableFact(typeof(MatrixException))] + public async Task GetTopicAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var topic = await room.GetTopicAsync(); + Assert.NotNull(topic); + Assert.NotNull(topic.Topic); + Assert.NotEmpty(topic.Topic); + + await room.LeaveAsync(); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests/RoomEventTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests/RoomEventTests.cs new file mode 100644 index 0000000..9081a5a --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RoomTests/RoomEventTests.cs @@ -0,0 +1,150 @@ +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 RoomEventTests : TestBed { + private readonly HomeserverAbstraction _hsAbstraction; + + public RoomEventTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _hsAbstraction = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); + } + + [Fact] + public async Task GetNameAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var name = await room.GetNameAsync(); + Assert.NotNull(name); + Assert.NotEmpty(name); + } + + [SkippableFact(typeof(MatrixException))] + public async Task GetTopicAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var topic = await room.GetTopicAsync(); + Assert.NotNull(topic); + Assert.NotNull(topic.Topic); + Assert.NotEmpty(topic.Topic); + } + + [SkippableFact(typeof(MatrixException))] + public async Task GetCanonicalAliasAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var alias = await room.GetCanonicalAliasAsync(); + Assert.NotNull(alias); + Assert.NotNull(alias.Alias); + Assert.NotEmpty(alias.Alias); + } + + [SkippableFact(typeof(MatrixException))] + public async Task GetAvatarUrlAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var url = await room.GetAvatarUrlAsync(); + Assert.NotNull(url); + Assert.NotNull(url.Url); + Assert.NotEmpty(url.Url); + + await room.LeaveAsync(); + } + + [Fact] + public async Task GetJoinRuleAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var rule = await room.GetJoinRuleAsync(); + Assert.NotNull(rule); + Assert.NotNull(rule.JoinRuleValue); + Assert.NotEmpty(rule.JoinRuleValue); + + await room.LeaveAsync(); + } + + [Fact] + public async Task GetHistoryVisibilityAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var visibility = await room.GetHistoryVisibilityAsync(); + Assert.NotNull(visibility); + Assert.NotNull(visibility.HistoryVisibility); + Assert.NotEmpty(visibility.HistoryVisibility); + + await room.LeaveAsync(); + } + + [Fact] + public async Task GetGuestAccessAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + try { + var access = await room.GetGuestAccessAsync(); + Assert.NotNull(access); + Assert.NotNull(access.GuestAccess); + Assert.NotEmpty(access.GuestAccess); + } + catch (Exception e) { + if (e is not MatrixException exception) throw; + Assert.Equal("M_NOT_FOUND", exception.ErrorCode); + } + + await room.LeaveAsync(); + } + + [Fact] + public async Task GetCreateEventAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var create = await room.GetCreateEventAsync(); + Assert.NotNull(create); + Assert.NotNull(create.Creator); + Assert.NotEmpty(create.RoomVersion!); + + await room.LeaveAsync(); + } + + [Fact] + public async Task GetRoomType() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + await room.GetRoomType(); + + await room.LeaveAsync(); + } + + [Fact] + public async Task GetPowerLevelsAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var power = await room.GetPowerLevelsAsync(); + Assert.NotNull(power); + Assert.NotNull(power.Ban); + Assert.NotNull(power.Kick); + Assert.NotNull(power.Invite); + Assert.NotNull(power.Redact); + Assert.NotNull(power.StateDefault); + Assert.NotNull(power.EventsDefault); + Assert.NotNull(power.UsersDefault); + Assert.NotNull(power.Users); + // Assert.NotNull(power.Events); + + await room.LeaveAsync(); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests/RoomMembershipTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests/RoomMembershipTests.cs new file mode 100644 index 0000000..2e552e2 --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RoomTests/RoomMembershipTests.cs @@ -0,0 +1,178 @@ +using System.Diagnostics; +using System.Text; +using ArcaneLibs.Extensions; +using LibMatrix.EventTypes.Spec.State; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +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 RoomMembershipTests : TestBed { + private readonly HomeserverAbstraction _hsAbstraction; + + public RoomMembershipTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _hsAbstraction = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); + } + + [Fact] + public async Task GetMembersAsync() { + Assert.True(StateEvent.KnownStateEventTypes is { Count: > 0 }, "StateEvent.KnownStateEventTypes is empty!"); + Assert.True(StateEvent.KnownStateEventTypesByName is { Count: > 0 }, "StateEvent.KnownStateEventTypesByName is empty!"); + + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var members = room.GetMembersEnumerableAsync(); + Assert.NotNull(members); + var hitMembers = false; + await foreach (var member in members) { + Assert.NotNull(member); + Assert.NotNull(member.StateKey); + Assert.NotEmpty(member.StateKey); + Assert.NotNull(member.Sender); + Assert.NotEmpty(member.Sender); + Assert.NotNull(member.RawContent); + Assert.NotEmpty(member.RawContent); + Assert.NotNull(member.TypedContent); + Assert.IsType(member.TypedContent); + var content = (RoomMemberEventContent)member.TypedContent; + Assert.NotNull(content); + Assert.NotNull(content.Membership); + Assert.NotEmpty(content.Membership); + hitMembers = true; + } + + Assert.True(hitMembers, "No members were found in the room"); + + await room.LeaveAsync(); + } + + [Fact] + public async Task JoinAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(_testOutputHelper); + var hs2 = await _hsAbstraction.GetNewHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + await room.SendStateEventAsync(RoomJoinRulesEventContent.EventId, new RoomJoinRulesEventContent() { + JoinRule = RoomJoinRulesEventContent.JoinRules.Public + }); + // var id = await room.JoinAsync(); + var id = await hs2.GetRoom(room.RoomId).JoinAsync(); + Assert.NotNull(id); + Assert.NotNull(id.RoomId); + Assert.NotEmpty(id.RoomId); + + await room.LeaveAsync(); + } + + [Fact] + public async Task ForgetAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + await room.ForgetAsync(); + } + + [Fact] + public async Task LeaveAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + await room.LeaveAsync(); + } + + [Fact] + public async Task KickAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var hs2 = await _hsAbstraction.GetNewHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + await room.InviteUserAsync(hs2.UserId, "Unit test!"); + await hs2.GetRoom(room.RoomId).JoinAsync(); + await room.KickAsync(hs2.UserId, "test"); + var banState = await room.GetStateAsync("m.room.member", hs2.UserId); + Assert.NotNull(banState); + Assert.Equal("leave", banState.Membership); + Assert.Equal("test", banState.Reason); + + await room.LeaveAsync(); + } + + [Fact] + public async Task BanAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var hs2 = await _hsAbstraction.GetNewHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + await room.BanAsync(hs2.UserId, "test"); + var banState = await room.GetStateAsync("m.room.member", hs2.UserId); + Assert.NotNull(banState); + Assert.Equal("ban", banState.Membership); + + await room.LeaveAsync(); + } + + [Fact] + public async Task UnbanAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var hs2 = await _hsAbstraction.GetNewHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + await room.BanAsync(hs2.UserId, "test"); + var banState = await room.GetStateAsync("m.room.member", hs2.UserId); + Assert.NotNull(banState); + Assert.Equal("ban", banState.Membership); + await room.UnbanAsync(hs2.UserId, "testing"); + + var unbanState = await room.GetStateAsync("m.room.member", hs2.UserId); + Assert.NotNull(unbanState); + Assert.Equal("leave", unbanState.Membership); + Assert.Equal("testing", unbanState.Reason); + + await room.LeaveAsync(); + } + + [Fact] + public async Task InviteAndJoinAsync() { + int count = 5; + + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + var otherUsers = _hsAbstraction.GetNewHomeservers(count); + Assert.NotNull(room); + + // var expectedCount = 1; + + // var tasks = new List(); + // await foreach (var otherUser in otherUsers) + // tasks.AddRange([ + // room.InviteUserAsync(otherUser.UserId), + // otherUser.GetRoom(room.RoomId).JoinAsync() + // ]); + + Dictionary tasks = new(); + await foreach (var otherUser in otherUsers) { + _testOutputHelper.WriteLine($"Inviting {otherUser.UserId} to {room.RoomId}"); + tasks.Add(otherUser, room.InviteUserAsync(otherUser.UserId, "Unit test!")); + } + + await foreach (var otherUser in tasks.ToAsyncEnumerable()) { + _testOutputHelper.WriteLine($"Joining {otherUser.UserId} to {room.RoomId}"); + await otherUser.GetRoom(room.RoomId).JoinAsync(reason: "Unit test!"); + } + + var states = await room.GetMembersListAsync(false); + Assert.Equal(count + 1, states.Count); + + await room.LeaveAsync(); + await foreach (var authenticatedHomeserverGeneric in otherUsers) + { + await authenticatedHomeserverGeneric.GetRoom(room.RoomId).LeaveAsync(); + } + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests/RoomTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests/RoomTests.cs new file mode 100644 index 0000000..401b24f --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RoomTests/RoomTests.cs @@ -0,0 +1,320 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using ArcaneLibs.Extensions; +using LibMatrix.EventTypes.Spec; +using LibMatrix.EventTypes.Spec.State; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +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 RoomTests : TestBed { + private readonly HomeserverAbstraction _hsAbstraction; + + public RoomTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _hsAbstraction = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); + } + + [Fact] + public async Task GetJoinedRoomsAsync() { + var hs = await _hsAbstraction.GetNewHomeserver(); + //make 100 rooms + var createRoomTasks = Enumerable.Range(0, 10).Select(_ => RoomAbstraction.GetTestRoom(hs)).ToList(); + await Task.WhenAll(createRoomTasks); + + var rooms = await hs.GetJoinedRooms(); + Assert.NotNull(rooms); + Assert.NotEmpty(rooms); + Assert.All(rooms, Assert.NotNull); + // Assert.True(rooms.Count >= 10, "Not enough rooms were found"); + Assert.Equal(10, rooms.Count); + await hs.Logout(); + } + + [Fact] + public async Task GetMembersAsync() { + Assert.True(StateEvent.KnownStateEventTypes is { Count: > 0 }, "StateEvent.KnownStateEventTypes is empty!"); + Assert.True(StateEvent.KnownStateEventTypesByName is { Count: > 0 }, "StateEvent.KnownStateEventTypesByName is empty!"); + + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var members = room.GetMembersEnumerableAsync(); + Assert.NotNull(members); + var hitMembers = false; + await foreach (var member in members) { + Assert.NotNull(member); + Assert.NotNull(member.StateKey); + Assert.NotEmpty(member.StateKey); + Assert.NotNull(member.Sender); + Assert.NotEmpty(member.Sender); + Assert.NotNull(member.RawContent); + Assert.NotEmpty(member.RawContent); + Assert.NotNull(member.TypedContent); + Assert.IsType(member.TypedContent); + var content = (RoomMemberEventContent)member.TypedContent; + Assert.NotNull(content); + Assert.NotNull(content.Membership); + Assert.NotEmpty(content.Membership); + hitMembers = true; + } + + Assert.True(hitMembers, "No members were found in the room"); + + await room.LeaveAsync(); + } + + [SkippableFact(typeof(MatrixException))] + public async Task SendStateEventAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", new UserProfileResponse() { + DisplayName = "wee_woo", + AvatarUrl = "no" + }); + await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", "state_key_maybe", new UserProfileResponse() { + DisplayName = "wee_woo", + AvatarUrl = "yes" + }); + + await room.LeaveAsync(); + } + + [SkippableFact(typeof(MatrixException))] + public async Task SendAndGetStateEventAsync() { + await SendStateEventAsync(); + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", new UserProfileResponse() { + DisplayName = "wee_woo", + AvatarUrl = "no" + }); + await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", "state_key_maybe", new UserProfileResponse() { + DisplayName = "wee_woo", + AvatarUrl = "yes" + }); + + var state1 = await room.GetStateAsync("gay.rory.libmatrix.unit_tests"); + Assert.NotNull(state1); + Assert.NotNull(state1.DisplayName); + Assert.NotEmpty(state1.DisplayName); + Assert.NotNull(state1.AvatarUrl); + Assert.NotEmpty(state1.AvatarUrl); + Assert.Equal("wee_woo", state1.DisplayName); + Assert.Equal("no", state1.AvatarUrl); + + var state2 = await room.GetStateAsync("gay.rory.libmatrix.unit_tests", "state_key_maybe"); + Assert.NotNull(state2); + Assert.NotNull(state2.DisplayName); + Assert.NotEmpty(state2.DisplayName); + Assert.NotNull(state2.AvatarUrl); + Assert.NotEmpty(state2.AvatarUrl); + Assert.Equal("wee_woo", state2.DisplayName); + Assert.Equal("yes", state2.AvatarUrl); + + await room.LeaveAsync(); + } + + [Fact] + public async Task DisbandAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + await room.PermanentlyBrickRoomAsync(); + + await room.LeaveAsync(); + } + + [Fact] + public async Task SendFileAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var res = await room.SendFileAsync("test.txt", new MemoryStream(Encoding.UTF8.GetBytes("This test was written by Emma [it/its], member of the Rory& system." + + "\nIf you are reading this on matrix, it means the unit test for uploading a file works!"))); + Assert.NotNull(res); + Assert.NotNull(res.EventId); + + await room.LeaveAsync(); + } + + [Fact] + public async Task GetFullStateAsListAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var state = await room.GetFullStateAsListAsync(); + Assert.NotNull(state); + Assert.NotEmpty(state); + Assert.All(state, Assert.NotNull); + Assert.All(state, s => { + Assert.NotNull(s.EventId); + Assert.NotEmpty(s.EventId); + Assert.NotNull(s.Sender); + Assert.NotEmpty(s.Sender); + Assert.NotNull(s.RawContent); + Assert.NotNull(s.TypedContent); + }); + + await room.LeaveAsync(); + } + + [SkippableFact(typeof(LibMatrixException))] + public async Task GetStateEventAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var state = await room.GetStateEventAsync("m.room.name"); + Assert.NotNull(state); + Assert.NotNull(state.EventId); + Assert.NotEmpty(state.EventId); + Assert.NotNull(state.Sender); + Assert.NotEmpty(state.Sender); + Assert.NotNull(state.RawContent); + Assert.NotEmpty(state.RawContent); + Assert.NotNull(state.TypedContent); + + await room.LeaveAsync(); + } + + [Fact] + public async Task GetStateEventIdAsync() { + var hs = await _hsAbstraction.GetNewHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var state = await room.GetStateEventIdAsync("m.room.name"); + Assert.NotNull(state); + Assert.NotEmpty(state); + + await room.LeaveAsync(); + } + + [SkippableFact(typeof(LibMatrixException))] + public async Task GetStateEventOrNullAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var state = await room.GetStateEventOrNullAsync("m.room.name"); + Assert.NotNull(state); + Assert.NotNull(state.EventId); + Assert.NotEmpty(state.EventId); + Assert.NotNull(state.Sender); + Assert.NotEmpty(state.Sender); + Assert.NotNull(state.RawContent); + Assert.NotEmpty(state.RawContent); + Assert.NotNull(state.TypedContent); + + await room.LeaveAsync(); + } + + [Fact] + public async Task GetMessagesAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var messages = await room.GetMessagesAsync(); + Assert.NotNull(messages); + Assert.NotNull(messages.Chunk); + Assert.NotEmpty(messages.Chunk); + Assert.All(messages.Chunk, Assert.NotNull); + Assert.All(messages.Chunk, m => { + Assert.NotNull(m.EventId); + Assert.NotEmpty(m.EventId); + Assert.NotNull(m.Sender); + Assert.NotEmpty(m.Sender); + Assert.NotNull(m.RawContent); + Assert.NotNull(m.TypedContent); + }); + + await room.LeaveAsync(); + + await File.WriteAllTextAsync("test.json", messages.ToJson()); + } + + [Fact] + public async Task GetManyMessagesAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var messages = room.GetManyMessagesAsync(chunkSize: 2); + await foreach (var resp in messages) { + Assert.NotNull(resp); + Assert.NotNull(resp.Chunk); + // Assert.NotEmpty(resp.Chunk); + Assert.All(resp.Chunk, Assert.NotNull); + Assert.All(resp.Chunk, m => { + Assert.NotNull(m.EventId); + Assert.NotEmpty(m.EventId); + Assert.NotNull(m.Sender); + Assert.NotEmpty(m.Sender); + Assert.NotNull(m.RawContent); + Assert.NotNull(m.TypedContent); + }); + } + + await room.LeaveAsync(); + } + + [Fact] + public async Task SendMessageEventAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var res = await room.SendMessageEventAsync(new RoomMessageEventContent(body: "This test was written by Emma [it/its], member of the Rory& system." + + "\nIf you are reading this on matrix, it means the unit test for sending a message works!", messageType: "m.text")); + Assert.NotNull(res); + Assert.NotNull(res.EventId); + + await room.LeaveAsync(); + } + + [Fact] + public async Task InviteUsersAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var users = _hsAbstraction.GetNewHomeservers(32).ToBlockingEnumerable().ToList(); + Assert.NotNull(users); + Assert.NotEmpty(users); + Assert.All(users, Assert.NotNull); + Assert.All(users, u => { + Assert.NotNull(u); + Assert.NotNull(u.UserId); + Assert.NotEmpty(u.UserId); + }); + + await room.InviteUsersAsync(users.Select(u => u.UserId)); + var members = await room.GetMembersListAsync(false); + Assert.NotNull(members); + Assert.NotEmpty(members); + Assert.All(members, Assert.NotNull); + Assert.All(members, m => { + Assert.NotNull(m); + Assert.NotNull(m.StateKey); + Assert.NotEmpty(m.StateKey); + }); + Assert.All(users, u => Assert.Contains(u.UserId, members.Select(m => m.StateKey))); + + await room.LeaveAsync(); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests/SpaceTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests/SpaceTests.cs new file mode 100644 index 0000000..148b5fe --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RoomTests/SpaceTests.cs @@ -0,0 +1,101 @@ +using System.Diagnostics; +using System.Text; +using ArcaneLibs.Extensions; +using LibMatrix.EventTypes.Spec.State; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +using LibMatrix.RoomTypes; +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 SpaceTests : TestBed { + private readonly HomeserverAbstraction _hsAbstraction; + + public SpaceTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _hsAbstraction = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); + } + + [Fact] + public async Task AddChildAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var crq = new CreateRoomRequest() { + Name = "Test space" + }; + crq.CreationContent["type"] = SpaceRoom.TypeName; + var space = (await hs.CreateRoom(crq)).AsSpace; + + var child = await hs.CreateRoom(new CreateRoomRequest() { + Name = "Test child" + }); + + await space.AddChildAsync(child); + + //validate children + var children = space.GetChildrenAsync().ToBlockingEnumerable().ToList(); + Assert.NotNull(children); + Assert.NotEmpty(children); + Assert.Single(children, x => x.RoomId == child.RoomId); + } + + [Fact] + public async Task AddChildByIdAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var crq = new CreateRoomRequest() { + Name = "Test space" + }; + crq.CreationContent["type"] = SpaceRoom.TypeName; + var space = (await hs.CreateRoom(crq)).AsSpace; + + var child = await hs.CreateRoom(new CreateRoomRequest() { + Name = "Test child" + }); + + await space.AddChildByIdAsync(child.RoomId); + + //validate children + var children = space.GetChildrenAsync().ToBlockingEnumerable().ToList(); + Assert.NotNull(children); + Assert.NotEmpty(children); + Assert.Single(children, x => x.RoomId == child.RoomId); + } + + [Fact] + public async Task GetChildrenAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var expectedChildren = Enumerable.Range(0, 10).Select(async _ => { + var room = await hs.CreateRoom(new CreateRoomRequest() { + Name = "Test child" + }); + return room; + }).ToAsyncEnumerable().ToBlockingEnumerable().ToList(); + + var crq = new CreateRoomRequest() { + Name = "Test space", + InitialState = expectedChildren.Select(c => new StateEvent() { + Type = "m.space.child", + StateKey = c.RoomId, + TypedContent = new SpaceChildEventContent() { + Via = new List { + c.RoomId.Split(":")[1] + } + } + }).ToList() + }; + crq.CreationContent["type"] = SpaceRoom.TypeName; + var space = (await hs.CreateRoom(crq)).AsSpace; + + var children = space.GetChildrenAsync().ToBlockingEnumerable().ToList(); + Assert.NotNull(children); + Assert.NotEmpty(children); + Assert.Equal(expectedChildren.Count, children.Count); + foreach (var expectedChild in expectedChildren) + { + Assert.Single(children, x => x.RoomId == expectedChild.RoomId); + } + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/TestCleanup.cs b/Tests/LibMatrix.Tests/Tests/TestCleanup.cs index 7fc7c64..1c5747c 100644 --- a/Tests/LibMatrix.Tests/Tests/TestCleanup.cs +++ b/Tests/LibMatrix.Tests/Tests/TestCleanup.cs @@ -1,74 +1,69 @@ -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 { - // private readonly TestFixture _fixture; - private readonly HomeserverResolverService _resolver; - private readonly Config _config; - private readonly HomeserverProviderService _provider; - private readonly ILogger _logger; - - public TestCleanup(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { - // _fixture = fixture; - _resolver = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); - _config = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); - _provider = _fixture.GetService(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}"); - _logger = _fixture.GetService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(ILogger)}"); - } - - [Fact] - 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 HomeserverAbstraction.GetHomeserver(); - 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 tasks = response.Rooms.Leave.Select(async room => { - await hs.GetRoom(room.Key).ForgetAsync(); - return room; - }).ToList(); - await Task.WhenAll(tasks); - } - }); - await syncHelper.RunSyncLoopAsync(cancellationToken: cancellationTokenSource.Token); - - Assert.NotNull(hs); - await hs.Logout(); - } -} \ No newline at end of file +// 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 { +// private readonly HomeserverAbstraction _hsAbstraction; +// private readonly ILogger _logger; +// +// public TestCleanup(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { +// // _fixture = fixture; +// _logger = _fixture.GetService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(ILogger)}"); +// _hsAbstraction = _fixture.GetService(_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 diff --git a/Tests/TestDataGenerator/Program.cs b/Tests/TestDataGenerator/Program.cs index 2583817..1168cae 100644 --- a/Tests/TestDataGenerator/Program.cs +++ b/Tests/TestDataGenerator/Program.cs @@ -9,12 +9,12 @@ using TestDataGenerator.Bot; Console.WriteLine("Hello, World!"); var host = Host.CreateDefaultBuilder(args).ConfigureServices((_, services) => { - services.AddScoped(_ => - new TieredStorageService( - new FileStorageProvider("bot_data/cache/"), - new FileStorageProvider("bot_data/data/") - ) - ); + // services.AddScoped(_ => + // new TieredStorageService( + // new FileStorageProvider("bot_data/cache/"), + // new FileStorageProvider("bot_data/data/") + // ) + // ); // services.AddSingleton(); services.AddSingleton(); diff --git a/Utilities/LibMatrix.DebugDataValidationApi/JsonElementExtensions.cs b/Utilities/LibMatrix.DebugDataValidationApi/JsonElementExtensions.cs new file mode 100644 index 0000000..c4ed743 --- /dev/null +++ b/Utilities/LibMatrix.DebugDataValidationApi/JsonElementExtensions.cs @@ -0,0 +1,147 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace LibMatrix.Extensions; + +public static class JsonElementExtensions { + public static bool FindExtraJsonElementFields(this JsonElement obj, Type objectType, string objectPropertyName) { + if (objectPropertyName == "content" && objectType == typeof(JsonObject)) + objectType = typeof(StateEventResponse); + // if (t == typeof(JsonNode)) + // return false; + + Console.WriteLine($"{objectType.Name} {objectPropertyName}"); + var unknownPropertyFound = false; + var mappedPropsDict = objectType.GetProperties() + .Where(x => x.GetCustomAttribute() is not null) + .ToDictionary(x => x.GetCustomAttribute()!.Name, x => x); + objectType.GetProperties().Where(x => !mappedPropsDict.ContainsKey(x.Name)) + .ToList().ForEach(x => mappedPropsDict.TryAdd(x.Name, x)); + + foreach (var field in obj.EnumerateObject()) { + if (mappedPropsDict.TryGetValue(field.Name, out var mappedProperty)) { + //dictionary + if (mappedProperty.PropertyType.IsGenericType && + mappedProperty.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { + unknownPropertyFound |= _checkDictionary(field, objectType, mappedProperty.PropertyType); + continue; + } + + if (mappedProperty.PropertyType.IsGenericType && + mappedProperty.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) { + unknownPropertyFound |= _checkList(field, objectType, mappedProperty.PropertyType); + continue; + } + + if (field.Name == "content" && (objectType == typeof(StateEventResponse) || objectType == typeof(StateEvent))) { + unknownPropertyFound |= field.FindExtraJsonPropertyFieldsByValueKind( + StateEvent.GetStateEventType(obj.GetProperty("type").GetString()!), // We expect type to always be present + mappedProperty.PropertyType); + continue; + } + + unknownPropertyFound |= + field.FindExtraJsonPropertyFieldsByValueKind(objectType, mappedProperty.PropertyType); + continue; + } + + Console.WriteLine($"[!!] Unknown property {field.Name} in {objectType.Name}!"); + unknownPropertyFound = true; + } + + return unknownPropertyFound; + } + + private static bool FindExtraJsonPropertyFieldsByValueKind(this JsonProperty field, Type containerType, + Type propertyType) { + if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) propertyType = propertyType.GetGenericArguments()[0]; + + var switchResult = false; + switch (field.Value.ValueKind) { + case JsonValueKind.Array: + switchResult = field.Value.EnumerateArray().Aggregate(switchResult, + (current, element) => current | element.FindExtraJsonElementFields(propertyType, field.Name)); + break; + case JsonValueKind.Object: + switchResult |= field.Value.FindExtraJsonElementFields(propertyType, field.Name); + break; + case JsonValueKind.True: + case JsonValueKind.False: + return _checkBool(field, containerType, propertyType); + case JsonValueKind.String: + return _checkString(field, containerType, propertyType); + case JsonValueKind.Number: + return _checkNumber(field, containerType, propertyType); + case JsonValueKind.Undefined: + case JsonValueKind.Null: + break; + default: + throw new ArgumentOutOfRangeException(); + } + + return switchResult; + } + + private static bool _checkBool(this JsonProperty field, Type containerType, Type propertyType) { + if (propertyType == typeof(bool)) return true; + Console.WriteLine( + $"[!!] Encountered bool for {field.Name} in {containerType.Name}, the class defines {propertyType.Name}!"); + return false; + } + + private static bool _checkString(this JsonProperty field, Type containerType, Type propertyType) { + if (propertyType == typeof(string)) return true; + // ReSharper disable once BuiltInTypeReferenceStyle + if (propertyType == typeof(String)) return true; + Console.WriteLine( + $"[!!] Encountered string for {field.Name} in {containerType.Name}, the class defines {propertyType.Name}!"); + return false; + } + + private static bool _checkNumber(this JsonProperty field, Type containerType, Type propertyType) { + if (propertyType == typeof(int) || + propertyType == typeof(double) || + propertyType == typeof(float) || + propertyType == typeof(decimal) || + propertyType == typeof(long) || + propertyType == typeof(short) || + propertyType == typeof(uint) || + propertyType == typeof(ulong) || + propertyType == typeof(ushort) || + propertyType == typeof(byte) || + propertyType == typeof(sbyte)) + return true; + Console.WriteLine( + $"[!!] Encountered number for {field.Name} in {containerType.Name}, the class defines {propertyType.Name}!"); + return false; + } + + private static bool _checkDictionary(this JsonProperty field, Type containerType, Type propertyType) { + var keyType = propertyType.GetGenericArguments()[0]; + var valueType = propertyType.GetGenericArguments()[1]; + valueType = Nullable.GetUnderlyingType(valueType) ?? valueType; + Console.WriteLine( + $"Encountered dictionary {field.Name} with key type {keyType.Name} and value type {valueType.Name}!"); + + return field.Value.EnumerateObject() + .Where(key => !valueType.IsPrimitive && valueType != typeof(string)) + .Aggregate(false, (current, key) => + current | key.FindExtraJsonPropertyFieldsByValueKind(containerType, valueType) + ); + } + + private static bool _checkList(this JsonProperty field, Type containerType, Type propertyType) { + var valueType = propertyType.GetGenericArguments()[0]; + valueType = Nullable.GetUnderlyingType(valueType) ?? valueType; + Console.WriteLine( + $"Encountered list {field.Name} with value type {valueType.Name}!"); + + return field.Value.EnumerateArray() + .Where(key => !valueType.IsPrimitive && valueType != typeof(string)) + .Aggregate(false, (current, key) => + current | key.FindExtraJsonElementFields(valueType, field.Name) + ); + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj b/Utilities/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj index 24fd617..3466fb1 100644 --- a/Utilities/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj +++ b/Utilities/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/Utilities/LibMatrix.DevTestBot/Bot/FileStorageProvider.cs b/Utilities/LibMatrix.DevTestBot/Bot/FileStorageProvider.cs index cc866e6..05b357e 100644 --- a/Utilities/LibMatrix.DevTestBot/Bot/FileStorageProvider.cs +++ b/Utilities/LibMatrix.DevTestBot/Bot/FileStorageProvider.cs @@ -1,36 +1,36 @@ -using System.Text.Json; -using ArcaneLibs.Extensions; -using LibMatrix.Interfaces.Services; -using Microsoft.Extensions.Logging; - -namespace LibMatrix.ExampleBot.Bot; - -public class FileStorageProvider : IStorageProvider { - private readonly ILogger _logger; - - public string TargetPath { get; } - - /// - /// Creates a new instance of . - /// - /// - public FileStorageProvider(string targetPath) { - new Logger(new LoggerFactory()).LogInformation("test"); - Console.WriteLine($"Initialised FileStorageProvider with path {targetPath}"); - TargetPath = targetPath; - if (!Directory.Exists(targetPath)) Directory.CreateDirectory(targetPath); - } - - public async Task SaveObjectAsync(string key, T value) => await File.WriteAllTextAsync(Path.Join(TargetPath, key), value?.ToJson()); - - public async Task LoadObjectAsync(string key) => JsonSerializer.Deserialize(await File.ReadAllTextAsync(Path.Join(TargetPath, key))); - - public Task ObjectExistsAsync(string key) => Task.FromResult(File.Exists(Path.Join(TargetPath, key))); - - public Task> GetAllKeysAsync() => Task.FromResult(Directory.GetFiles(TargetPath).Select(Path.GetFileName).ToList()); - - public Task DeleteObjectAsync(string key) { - File.Delete(Path.Join(TargetPath, key)); - return Task.CompletedTask; - } -} \ No newline at end of file +// using System.Text.Json; +// using ArcaneLibs.Extensions; +// using LibMatrix.Interfaces.Services; +// using Microsoft.Extensions.Logging; +// +// namespace LibMatrix.ExampleBot.Bot; +// +// public class FileStorageProvider : IStorageProvider { +// private readonly ILogger _logger; +// +// public string TargetPath { get; } +// +// /// +// /// Creates a new instance of . +// /// +// /// +// public FileStorageProvider(string targetPath) { +// new Logger(new LoggerFactory()).LogInformation("test"); +// Console.WriteLine($"Initialised FileStorageProvider with path {targetPath}"); +// TargetPath = targetPath; +// if (!Directory.Exists(targetPath)) Directory.CreateDirectory(targetPath); +// } +// +// public async Task SaveObjectAsync(string key, T value) => await File.WriteAllTextAsync(Path.Join(TargetPath, key), value?.ToJson()); +// +// public async Task LoadObjectAsync(string key) => JsonSerializer.Deserialize(await File.ReadAllTextAsync(Path.Join(TargetPath, key))); +// +// public Task ObjectExistsAsync(string key) => Task.FromResult(File.Exists(Path.Join(TargetPath, key))); +// +// public Task> GetAllKeysAsync() => Task.FromResult(Directory.GetFiles(TargetPath).Select(Path.GetFileName).ToList()); +// +// public Task DeleteObjectAsync(string key) { +// File.Delete(Path.Join(TargetPath, key)); +// return Task.CompletedTask; +// } +// } \ No newline at end of file diff --git a/Utilities/LibMatrix.Utilities.Bot/FileStorageProvider.cs b/Utilities/LibMatrix.Utilities.Bot/FileStorageProvider.cs index b762937..f723c57 100644 --- a/Utilities/LibMatrix.Utilities.Bot/FileStorageProvider.cs +++ b/Utilities/LibMatrix.Utilities.Bot/FileStorageProvider.cs @@ -1,32 +1,32 @@ -using System.Text.Json; -using ArcaneLibs.Extensions; -using LibMatrix.Interfaces.Services; - -namespace LibMatrix.Utilities.Bot; - -public class FileStorageProvider : IStorageProvider { - public string TargetPath { get; } - - /// - /// Creates a new instance of . - /// - /// - public FileStorageProvider(string targetPath) { - Console.WriteLine($"Initialised FileStorageProvider with path {targetPath}"); - TargetPath = targetPath; - if (!Directory.Exists(targetPath)) Directory.CreateDirectory(targetPath); - } - - public async Task SaveObjectAsync(string key, T value) => await File.WriteAllTextAsync(Path.Join(TargetPath, key), value?.ToJson()); - - public async Task LoadObjectAsync(string key) => JsonSerializer.Deserialize(await File.ReadAllTextAsync(Path.Join(TargetPath, key))); - - public Task ObjectExistsAsync(string key) => Task.FromResult(File.Exists(Path.Join(TargetPath, key))); - - public Task> GetAllKeysAsync() => Task.FromResult(Directory.GetFiles(TargetPath).Select(Path.GetFileName).ToList()); - - public Task DeleteObjectAsync(string key) { - File.Delete(Path.Join(TargetPath, key)); - return Task.CompletedTask; - } -} \ No newline at end of file +// using System.Text.Json; +// using ArcaneLibs.Extensions; +// using LibMatrix.Interfaces.Services; +// +// namespace LibMatrix.Utilities.Bot; +// +// public class FileStorageProvider : IStorageProvider { +// public string TargetPath { get; } +// +// /// +// /// Creates a new instance of . +// /// +// /// +// public FileStorageProvider(string targetPath) { +// Console.WriteLine($"Initialised FileStorageProvider with path {targetPath}"); +// TargetPath = targetPath; +// if (!Directory.Exists(targetPath)) Directory.CreateDirectory(targetPath); +// } +// +// public async Task SaveObjectAsync(string key, T value) => await File.WriteAllTextAsync(Path.Join(TargetPath, key), value?.ToJson()); +// +// public async Task LoadObjectAsync(string key) => JsonSerializer.Deserialize(await File.ReadAllTextAsync(Path.Join(TargetPath, key))); +// +// public Task ObjectExistsAsync(string key) => Task.FromResult(File.Exists(Path.Join(TargetPath, key))); +// +// public Task> GetAllKeysAsync() => Task.FromResult(Directory.GetFiles(TargetPath).Select(Path.GetFileName).ToList()); +// +// public Task DeleteObjectAsync(string key) { +// File.Delete(Path.Join(TargetPath, key)); +// return Task.CompletedTask; +// } +// } \ No newline at end of file diff --git a/Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj b/Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj index 89ea5af..6e67373 100644 --- a/Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj +++ b/Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj @@ -12,9 +12,9 @@ - + - + -- cgit 1.4.1