From 31ca17d53602fc4d6078f51595a8f41a6ba4d4b2 Mon Sep 17 00:00:00 2001 From: Rory& Date: Fri, 5 Jun 2026 15:48:02 +0200 Subject: [PATCH] Some basic unit tests --- extra/admin-api/SpacebarAdminAPI.slnx | 3 + .../Abstractions/UserAbstraction.cs | 24 +++++ .../admin-api/Tests/Spacebar.Tests/Config.cs | 11 +++ .../Extensions/AssertExtensions.cs | 15 +++ .../Extensions/AssertHttpExtensions.cs | 38 ++++++++ .../Spacebar.Tests/Fixtures/TestFixture.cs | 29 ++++++ .../Tests/Spacebar.Tests/GlobalUsings.cs | 1 + .../Spacebar.Tests/Spacebar.Tests.csproj | 44 +++++++++ .../Tests/AuthenticationTests.cs | 95 +++++++++++++++++++ .../Tests/BasicWellKnownTests.cs | 44 +++++++++ .../Spacebar.Tests/Tests/ChannelTests.cs | 65 +++++++++++++ .../Tests/Spacebar.Tests/Tests/GuildTests.cs | 47 +++++++++ .../Tests/Meta/UserAbstractionTests.cs | 16 ++++ .../Tests/Spacebar.Tests/appsettings.json | 5 + 14 files changed, 437 insertions(+) create mode 100644 extra/admin-api/Tests/Spacebar.Tests/Abstractions/UserAbstraction.cs create mode 100644 extra/admin-api/Tests/Spacebar.Tests/Config.cs create mode 100644 extra/admin-api/Tests/Spacebar.Tests/Extensions/AssertExtensions.cs create mode 100644 extra/admin-api/Tests/Spacebar.Tests/Extensions/AssertHttpExtensions.cs create mode 100644 extra/admin-api/Tests/Spacebar.Tests/Fixtures/TestFixture.cs create mode 100644 extra/admin-api/Tests/Spacebar.Tests/GlobalUsings.cs create mode 100644 extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj create mode 100644 extra/admin-api/Tests/Spacebar.Tests/Tests/AuthenticationTests.cs create mode 100644 extra/admin-api/Tests/Spacebar.Tests/Tests/BasicWellKnownTests.cs create mode 100644 extra/admin-api/Tests/Spacebar.Tests/Tests/ChannelTests.cs create mode 100644 extra/admin-api/Tests/Spacebar.Tests/Tests/GuildTests.cs create mode 100644 extra/admin-api/Tests/Spacebar.Tests/Tests/Meta/UserAbstractionTests.cs create mode 100644 extra/admin-api/Tests/Spacebar.Tests/appsettings.json diff --git a/extra/admin-api/SpacebarAdminAPI.slnx b/extra/admin-api/SpacebarAdminAPI.slnx index 44498b0f6..491568cd5 100644 --- a/extra/admin-api/SpacebarAdminAPI.slnx +++ b/extra/admin-api/SpacebarAdminAPI.slnx @@ -25,6 +25,9 @@ + + + diff --git a/extra/admin-api/Tests/Spacebar.Tests/Abstractions/UserAbstraction.cs b/extra/admin-api/Tests/Spacebar.Tests/Abstractions/UserAbstraction.cs new file mode 100644 index 000000000..b6cef1ac9 --- /dev/null +++ b/extra/admin-api/Tests/Spacebar.Tests/Abstractions/UserAbstraction.cs @@ -0,0 +1,24 @@ +using Spacebar.Sdk.Core; + +namespace Spacebar.Tests.Abstractions; + +public class UserAbstraction(Config _config, SpacebarClientProviderService _clientProvider) { + public async Task GetFreshUser(bool withAutojoinGuilds = false) { + var ua = await _clientProvider.GetUnauthenticatedClientAsync(_config.TestInstance); + var tokenResponse = await ua.RegisterAsync(new() { + Email = $"{Guid.NewGuid().ToString()}@{Guid.NewGuid().ToString()}.tld", + Username = Guid.NewGuid().ToString(), + Password = Guid.NewGuid().ToString(), + DateOfBirth = new(), + Consent = true + }); + var client = await _clientProvider.GetAuthenticatedClientAsync(_config.TestInstance, tokenResponse.Token); + + if (!withAutojoinGuilds) { + + } + + return client; + } + +} \ No newline at end of file diff --git a/extra/admin-api/Tests/Spacebar.Tests/Config.cs b/extra/admin-api/Tests/Spacebar.Tests/Config.cs new file mode 100644 index 000000000..d1aa9de04 --- /dev/null +++ b/extra/admin-api/Tests/Spacebar.Tests/Config.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.Configuration; + +namespace Spacebar.Tests; + +public class Config { + public Config(IConfiguration? config) { + config.GetSection("Configuration").Bind(this); + } + + public string TestInstance { get; set; } +} \ No newline at end of file diff --git a/extra/admin-api/Tests/Spacebar.Tests/Extensions/AssertExtensions.cs b/extra/admin-api/Tests/Spacebar.Tests/Extensions/AssertExtensions.cs new file mode 100644 index 000000000..fa146f786 --- /dev/null +++ b/extra/admin-api/Tests/Spacebar.Tests/Extensions/AssertExtensions.cs @@ -0,0 +1,15 @@ +namespace Spacebar.Tests.Extensions; + +public static class AssertExtensions { + extension(Assert) { + public static void StringNotNullOrEmpty(string? str) { + Assert.NotNull(str); + Assert.NotEqual("", str); + } + + public static void StringNotNullOrWhitespace(string? str) { + StringNotNullOrEmpty(str); + Assert.Matches(".+", str); + } + } +} \ No newline at end of file diff --git a/extra/admin-api/Tests/Spacebar.Tests/Extensions/AssertHttpExtensions.cs b/extra/admin-api/Tests/Spacebar.Tests/Extensions/AssertHttpExtensions.cs new file mode 100644 index 000000000..f5e99dc98 --- /dev/null +++ b/extra/admin-api/Tests/Spacebar.Tests/Extensions/AssertHttpExtensions.cs @@ -0,0 +1,38 @@ +using System.Net.Http.Json; +using System.Text.Json.Nodes; + +namespace Spacebar.Tests.Extensions; + +public static class AssertHttpExtensions { + private static readonly HttpClient Hc = new(); + + public static async Task GetFormattedErrorDetails(HttpResponseMessage res) { + return res.Content.Headers.ContentType?.MediaType == "application/json" + ? (await res.Content.ReadFromJsonAsync())!.ToJsonString(new() { + WriteIndented = true + }) + : await res.Content.ReadAsStringAsync(); + } + + extension(Assert) { + public static async Task SuccessfullyHttpGetAsync(string url) { + var res = await Hc.GetAsync(url); + Assert.True(res.IsSuccessStatusCode, $"Could not get {url}: {res.StatusCode}\n{await GetFormattedErrorDetails(res)}"); + return res; + } + + public static async Task SuccessfullyHttpPostAsJsonAsync(string url, TValue obj) { + var res = await Hc.PostAsJsonAsync(url, obj); + if (!res.IsSuccessStatusCode) + Assert.True(res.IsSuccessStatusCode, $"Could not POST JSON to {url}: {(int)res.StatusCode} {res.StatusCode}\n{await GetFormattedErrorDetails(res)}"); + return res; + } + + public static async Task HttpSuccess(HttpResponseMessage res) { + if (!res.IsSuccessStatusCode) + Assert.True(res.IsSuccessStatusCode, + $"Could not {res.RequestMessage!.Method.Method.ToUpper()} to {res.RequestMessage!.RequestUri!.ToString()}: {(int)res.StatusCode} {res.StatusCode}\n{await GetFormattedErrorDetails(res)}"); + return res; + } + } +} \ No newline at end of file diff --git a/extra/admin-api/Tests/Spacebar.Tests/Fixtures/TestFixture.cs b/extra/admin-api/Tests/Spacebar.Tests/Fixtures/TestFixture.cs new file mode 100644 index 000000000..cc9ce056f --- /dev/null +++ b/extra/admin-api/Tests/Spacebar.Tests/Fixtures/TestFixture.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Spacebar.Sdk.Core; +using Spacebar.Tests.Abstractions; +using Xunit.Microsoft.DependencyInjection; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace Spacebar.Tests.Fixtures; + +public class TestFixture : TestBedFixture { + protected override void AddServices(IServiceCollection services, IConfiguration configuration) { + services.AddSingleton(configuration); + services.AddLogging(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + + } + + protected override ValueTask DisposeAsyncCore() + => new(); + + protected override IEnumerable GetTestAppSettings() { + yield return new TestAppSettings { Filename = "appsettings.json", IsOptional = true }; + } +} \ No newline at end of file diff --git a/extra/admin-api/Tests/Spacebar.Tests/GlobalUsings.cs b/extra/admin-api/Tests/Spacebar.Tests/GlobalUsings.cs new file mode 100644 index 000000000..8c927eb74 --- /dev/null +++ b/extra/admin-api/Tests/Spacebar.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj b/extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj new file mode 100644 index 000000000..b521b61c8 --- /dev/null +++ b/extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj @@ -0,0 +1,44 @@ + + + + net10.0 + enable + enable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + Always + + + + diff --git a/extra/admin-api/Tests/Spacebar.Tests/Tests/AuthenticationTests.cs b/extra/admin-api/Tests/Spacebar.Tests/Tests/AuthenticationTests.cs new file mode 100644 index 000000000..04bd5f912 --- /dev/null +++ b/extra/admin-api/Tests/Spacebar.Tests/Tests/AuthenticationTests.cs @@ -0,0 +1,95 @@ +using System.Net.Http.Json; +using System.Text.Json.Nodes; +using ArcaneLibs.Extensions; +using Spacebar.Models.Api; +using Spacebar.Sdk.Core; +using Spacebar.Tests.Extensions; +using Spacebar.Tests.Fixtures; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace Spacebar.Tests.Tests; + +public class AuthenticationTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : TestBed(testOutputHelper, fixture) { + private readonly Config _config = fixture.GetService(testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); + + private readonly SpacebarClientWellKnownResolverService _wellKnownResolver = fixture.GetService(testOutputHelper) ?? + throw new InvalidOperationException( + $"Failed to get {nameof(SpacebarClientWellKnownResolverService)}"); + + private readonly SpacebarClientProviderService _clientProvider = fixture.GetService(testOutputHelper) ?? + throw new InvalidOperationException($"Failed to get {nameof(SpacebarClientProviderService)}"); + + [Fact] + public async Task RegisterUser() { + var res = await Assert.SuccessfullyHttpPostAsJsonAsync($"{_config.TestInstance}/api/v9/auth/register", new RegisterRequest() { + Email = $"{Guid.NewGuid().ToString()}@{Guid.NewGuid().ToString()}.tld", + Username = Guid.NewGuid().ToString(), + Password = Guid.NewGuid().ToString(), + DateOfBirth = new(), + Consent = true + }); + } + + [Fact] + public async Task ConcurrentRegister50Users() { + var tasks = Enumerable.Range(0, 50).Select(async _ => { + var rr = new RegisterRequest() { + Email = $"{Guid.NewGuid().ToString()}@{Guid.NewGuid().ToString()}.tld", + Username = Guid.NewGuid().ToString(), + Password = "password", + DateOfBirth = new(), + Consent = true + }; + return (rr, await Assert.SuccessfullyHttpPostAsJsonAsync($"{_config.TestInstance}/api/v9/auth/register", rr)); + }).ToList(); + await Task.WhenAll(tasks); + + testOutputHelper.WriteLine("Waiting for server to settle..."); + await Task.Delay(2500, TestContext.Current.CancellationToken); + + testOutputHelper.WriteLine("Cleaning up users..."); + var cleanupTasks = tasks.Select(x => x.Result).Select(async res => { + var resp = await res.Item2.Content.ReadFromJsonAsync(); + var c = await _clientProvider.GetAuthenticatedClientAsync(_config.TestInstance, resp.Token); + var dresp = (await c.ApiHttpClient.PostAsJsonAsync("/api/v9/users/@me/delete", new JsonObject() { + { "password", "password" } + }, cancellationToken: TestContext.Current.CancellationToken)); + // TODO: figure out why this fails with "invalid password" + if (!dresp.IsSuccessStatusCode) + testOutputHelper.WriteLine("Failed to delete user: " + await AssertHttpExtensions.GetFormattedErrorDetails(dresp)); + }).ToList(); + await Task.WhenAll(cleanupTasks); + } + + [Fact] + public async Task LoginUser() { + var rr = new RegisterRequest() { + Email = $"{Guid.NewGuid().ToString()}@{Guid.NewGuid().ToString()}.tld", + Username = Guid.NewGuid().ToString(), + Password = Guid.NewGuid().ToString(), + DateOfBirth = new(), + Consent = true + }; + var rrRes = await Assert.SuccessfullyHttpPostAsJsonAsync($"{_config.TestInstance}/api/v9/auth/register", rr); + var loginRes = await Assert.SuccessfullyHttpPostAsJsonAsync($"{_config.TestInstance}/api/v9/auth/login", new LoginRequest() { + Login = rr.Email, + Password = rr.Password + }); + } + + [Fact] + public async Task WhoAmI() { + var rr = new RegisterRequest() { + Email = $"{Guid.NewGuid().ToString()}@{Guid.NewGuid().ToString()}.tld", + Username = Guid.NewGuid().ToString(), + Password = Guid.NewGuid().ToString(), + DateOfBirth = new(), + Consent = true + }; + var rrRes = await Assert.SuccessfullyHttpPostAsJsonAsync($"{_config.TestInstance}/api/v9/auth/register", rr); + var res = await rrRes.Content.ReadFromJsonAsync(); + var client = await _clientProvider.GetAuthenticatedClientAsync(_config.TestInstance, res.Token); + var waRes = await Assert.HttpSuccess(await client.ApiHttpClient.GetAsync("/api/v9/auth/whoami")); + // TODO: finish test once model exists + } +} \ No newline at end of file diff --git a/extra/admin-api/Tests/Spacebar.Tests/Tests/BasicWellKnownTests.cs b/extra/admin-api/Tests/Spacebar.Tests/Tests/BasicWellKnownTests.cs new file mode 100644 index 000000000..56254a627 --- /dev/null +++ b/extra/admin-api/Tests/Spacebar.Tests/Tests/BasicWellKnownTests.cs @@ -0,0 +1,44 @@ +using Spacebar.Sdk.Core; +using Spacebar.Tests.Extensions; +using Spacebar.Tests.Fixtures; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace Spacebar.Tests.Tests; + +public class BasicWellKnownTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : TestBed(testOutputHelper, fixture) { + private readonly Config _config = fixture.GetService(testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); + + private readonly SpacebarClientWellKnownResolverService _wellKnownResolver = fixture.GetService(testOutputHelper) ?? + throw new InvalidOperationException($"Failed to get {nameof(Config)}"); + + [Fact] + public async Task ValidateTestConfig() { + Assert.NotNull(_config.TestInstance); + Assert.NotEmpty(_config.TestInstance); + } + + [Fact] + public async Task CanReachInstance() => await Assert.SuccessfullyHttpGetAsync($"{_config.TestInstance}/api/v9/ping"); + + [Fact] + public async Task CanGetOldWellknown() { + await Assert.SuccessfullyHttpGetAsync($"{_config.TestInstance}/.well-known/spacebar"); + await Assert.SuccessfullyHttpGetAsync($"{_config.TestInstance}/api/v9/policies/instance/domains"); + } + + [Fact] + public async Task CanGetNewWellknown() => await Assert.SuccessfullyHttpGetAsync($"{_config.TestInstance}/.well-known/spacebar/client"); + + [Fact] + public async Task SdkCanGetWellKnown() { + testOutputHelper.WriteLine("instance: " + _config.TestInstance); + var res = await _wellKnownResolver.ResolveClientWellKnown(_config.TestInstance); + Assert.StringNotNullOrWhitespace(res.Api.BaseUrl); + Assert.StringNotNullOrWhitespace(res.Cdn.BaseUrl); + Assert.StringNotNullOrWhitespace(res.Gateway.BaseUrl); + Assert.NotEmpty(res.Api.ApiVersions.Active); + Assert.StringNotNullOrWhitespace(res.Api.ApiVersions.Default); + Assert.NotEmpty(res.Gateway.Compression); + Assert.NotEmpty(res.Gateway.Encoding); + } +} \ No newline at end of file diff --git a/extra/admin-api/Tests/Spacebar.Tests/Tests/ChannelTests.cs b/extra/admin-api/Tests/Spacebar.Tests/Tests/ChannelTests.cs new file mode 100644 index 000000000..d04ee994b --- /dev/null +++ b/extra/admin-api/Tests/Spacebar.Tests/Tests/ChannelTests.cs @@ -0,0 +1,65 @@ +using System.Net.Http.Json; +using Spacebar.Models.Generic; +using Spacebar.Sdk.Core; +using Spacebar.Tests.Abstractions; +using Spacebar.Tests.Extensions; +using Spacebar.Tests.Fixtures; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace Spacebar.Tests.Tests; + +public class ChannelTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : TestBed(testOutputHelper, fixture) { + private readonly Config _config = fixture.GetService(testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); + + private readonly SpacebarClientWellKnownResolverService _wellKnownResolver = fixture.GetService(testOutputHelper) ?? + throw new InvalidOperationException( + $"Failed to get {nameof(SpacebarClientWellKnownResolverService)}"); + + private readonly SpacebarClientProviderService _clientProvider = fixture.GetService(testOutputHelper) ?? + throw new InvalidOperationException($"Failed to get {nameof(SpacebarClientProviderService)}"); + + private readonly UserAbstraction _userAbstraction = fixture.GetService(testOutputHelper) ?? + throw new InvalidOperationException($"Failed to get {nameof(SpacebarClientProviderService)}"); + + [Fact] + public async Task CreateChannel() { + var client = await _userAbstraction.GetFreshUser(); + var guild = await client.CreateGuild(new() { + Name = "Test guild" + }); + + Assert.Equal("Test guild", guild.Name); + + var channel = await client.GetGuild(guild.Id).CreateChannelAsync(new() { + Name = "test", + Type = 0 + }); + + Assert.Equal("test", channel.Name); + } + + [Fact] + public async Task GetChannel() { + var client = await _userAbstraction.GetFreshUser(); + var guild = await client.CreateGuild(new() { + Name = "Test guild" + }); + + Assert.Equal("Test guild", guild.Name); + + var channel = await client.GetGuild(guild.Id).CreateChannelAsync(new() { + Name = "test", + Type = 0 + }); + + Assert.Equal("test", channel.Name); + + var res = await client.ApiHttpClient.GetAsync("channels/" + channel.Id, TestContext.Current.CancellationToken); + await Assert.HttpSuccess(res); + + var channelResp = await res.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Equal(channel.Name, channelResp!.Name); + Assert.Equal(channel.Id, channelResp!.Id); + + } +} \ No newline at end of file diff --git a/extra/admin-api/Tests/Spacebar.Tests/Tests/GuildTests.cs b/extra/admin-api/Tests/Spacebar.Tests/Tests/GuildTests.cs new file mode 100644 index 000000000..12e566f03 --- /dev/null +++ b/extra/admin-api/Tests/Spacebar.Tests/Tests/GuildTests.cs @@ -0,0 +1,47 @@ +using Spacebar.Sdk.Core; +using Spacebar.Tests.Abstractions; +using Spacebar.Tests.Extensions; +using Spacebar.Tests.Fixtures; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace Spacebar.Tests.Tests; + +public class GuildTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : TestBed(testOutputHelper, fixture) { + private readonly Config _config = fixture.GetService(testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); + + private readonly SpacebarClientWellKnownResolverService _wellKnownResolver = fixture.GetService(testOutputHelper) ?? + throw new InvalidOperationException( + $"Failed to get {nameof(SpacebarClientWellKnownResolverService)}"); + + private readonly SpacebarClientProviderService _clientProvider = fixture.GetService(testOutputHelper) ?? + throw new InvalidOperationException($"Failed to get {nameof(SpacebarClientProviderService)}"); + + private readonly UserAbstraction _userAbstraction = fixture.GetService(testOutputHelper) ?? + throw new InvalidOperationException($"Failed to get {nameof(SpacebarClientProviderService)}"); + + [Fact] + public async Task CreateGuild() { + var client = await _userAbstraction.GetFreshUser(); + var guild = await client.CreateGuild(new() { + Name = "Test guild" + }); + + Assert.Equal("Test guild", guild.Name); + } + + [Fact] + public async Task GetChannels() { + var client = await _userAbstraction.GetFreshUser(); + var guild = await client.CreateGuild(new() { + Name = "Test guild" + }); + + Assert.Equal("Test guild", guild.Name); + + var channels = await client.GetGuild(guild.Id).GetChannelsAsync(); + Assert.NotEmpty(channels); + foreach (var channel in channels) { + Assert.StringNotNullOrWhitespace(channel.Name); + } + } +} \ No newline at end of file diff --git a/extra/admin-api/Tests/Spacebar.Tests/Tests/Meta/UserAbstractionTests.cs b/extra/admin-api/Tests/Spacebar.Tests/Tests/Meta/UserAbstractionTests.cs new file mode 100644 index 000000000..81f53f7f3 --- /dev/null +++ b/extra/admin-api/Tests/Spacebar.Tests/Tests/Meta/UserAbstractionTests.cs @@ -0,0 +1,16 @@ +using Spacebar.Tests.Abstractions; +using Spacebar.Tests.Extensions; +using Spacebar.Tests.Fixtures; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace Spacebar.Tests.Tests.Meta; + +public class UserAbstractionTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : TestBed(testOutputHelper, fixture) { + private readonly UserAbstraction _config = fixture.GetService(testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(UserAbstraction)}"); + + [Fact] + public async Task CanGetUser() { + var res = await _config.GetFreshUser(); + Assert.StringNotNullOrWhitespace(res.ApiHttpClient.BaseAddress!.ToString()); + } +} \ No newline at end of file diff --git a/extra/admin-api/Tests/Spacebar.Tests/appsettings.json b/extra/admin-api/Tests/Spacebar.Tests/appsettings.json new file mode 100644 index 000000000..d1d45a93a --- /dev/null +++ b/extra/admin-api/Tests/Spacebar.Tests/appsettings.json @@ -0,0 +1,5 @@ +{ + "Configuration": { + "TestInstance": "http://localhost:3001" + } +} \ No newline at end of file