diff --git a/LANCommander.Server.Tests/ApplicationFactory.cs b/LANCommander.Server.Tests/ApplicationFactory.cs index 1f245ecf..192dc9ca 100644 --- a/LANCommander.Server.Tests/ApplicationFactory.cs +++ b/LANCommander.Server.Tests/ApplicationFactory.cs @@ -1,12 +1,18 @@ using System.Data.Common; +using System.Text; +using LANCommander.SDK.Providers; using LANCommander.Server.Data; using LANCommander.Server.Services.Abstractions; using LANCommander.Server.Tests.Mocks; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using ServerSettings = LANCommander.Server.Settings.Settings; namespace LANCommander.Server.Tests; @@ -54,6 +60,28 @@ public class ApplicationFactory services.AddSingleton(GitHubServiceMockFactory.Create()); #endregion + + #region JWT signing key alignment + // The server snapshots the JWT signing secret from configuration when AddIdentity runs, + // but ValidateSettings regenerates the secret at startup whenever it is missing (which is + // always the case in tests, where no persisted settings file provides one). That leaves the + // bearer validation key pinned to the empty pre-regeneration value while tokens are signed + // with the regenerated secret, so every authenticated API call fails with 401. Re-bind the + // validation key from the live settings provider once the regenerated secret exists. + services.AddSingleton, AlignJwtSigningKey>(); + #endregion }); } + + private sealed class AlignJwtSigningKey(SettingsProvider settingsProvider) + : IPostConfigureOptions + { + public void PostConfigure(string? name, JwtBearerOptions options) + { + var secret = settingsProvider.CurrentValue.Server.Authentication.TokenSecret; + + options.TokenValidationParameters.IssuerSigningKey = + new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); + } + } } \ No newline at end of file diff --git a/LANCommander.Server.Tests/ApplicationFixture.cs b/LANCommander.Server.Tests/ApplicationFixture.cs index f5d0fdb2..8e4365d1 100644 --- a/LANCommander.Server.Tests/ApplicationFixture.cs +++ b/LANCommander.Server.Tests/ApplicationFixture.cs @@ -1,28 +1,78 @@ +using LANCommander.SDK.Abstractions; +using LANCommander.SDK.Extensions; +using LANCommander.SDK.Providers; +using LANCommander.SDK.Services; using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; namespace LANCommander.Server.Tests; public class ApplicationFixture : ApplicationFactory { public static ApplicationFixture Instance; - - public SDK.Client Client { get; set; } + + /// Server-side service provider (the in-memory app under test). public IServiceProvider ServiceProvider { get; set; } + /// Client-side SDK service provider, wired to talk to the in-memory server. + public IServiceProvider ClientServiceProvider { get; set; } + + /// HttpClient whose handler routes to the in-memory server. + public HttpClient HttpClient { get; } + + public Uri ServerAddress { get; } + + public AuthenticationClient AuthenticationClient { get; } + public GameClient GameClient { get; } + public SaveClient SaveClient { get; } + public TagClient TagClient { get; } + public ApplicationFixture(ApplicationFactory factory) { if (Instance != null) return; - Client = new SDK.Client(factory.CreateClient(), "C:\\Games"); ServiceProvider = factory.Services; + HttpClient = factory.CreateClient(); + ServerAddress = HttpClient.BaseAddress!; + + // Build a separate SDK client container (mirrors the launcher's composition) whose + // injected HttpClient is the in-memory test handler, so all API calls route to the + // server under test instead of hitting the network. + var services = new ServiceCollection(); + + services.AddLogging(); + services.AddOptions().Configure(_ => { }); + services.AddSingleton(NoopRefresher.Instance); + services.AddLANCommanderClient(); + services.AddSingleton(HttpClient); + + ClientServiceProvider = services.BuildServiceProvider(); + + ClientServiceProvider.GetRequiredService().SetServerAddress(ServerAddress); + + AuthenticationClient = ClientServiceProvider.GetRequiredService(); + GameClient = ClientServiceProvider.GetRequiredService(); + SaveClient = ClientServiceProvider.GetRequiredService(); + TagClient = ClientServiceProvider.GetRequiredService(); + Instance = this; } + + /// Authenticates against the in-memory server and stores the token for subsequent client calls. + public Task AuthenticateAsync(string username, string password) + => AuthenticationClient.AuthenticateAsync(username, password, ServerAddress); + + private sealed class NoopRefresher : IServerConfigurationRefresher + { + public static readonly NoopRefresher Instance = new(); + public Task RefreshAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; + } } [CollectionDefinition("Application")] public class ApplicationCollection : ICollectionFixture> { - -} \ No newline at end of file + +} diff --git a/LANCommander.Server.Tests/BaseTest.cs b/LANCommander.Server.Tests/BaseTest.cs index f9fcae5e..651b1e49 100644 --- a/LANCommander.Server.Tests/BaseTest.cs +++ b/LANCommander.Server.Tests/BaseTest.cs @@ -1,5 +1,6 @@ using System.Formats.Asn1; using LANCommander.SDK.Enums; +using LANCommander.SDK.Services; using LANCommander.Server.Data.Models; using LANCommander.Server.Services; using Microsoft.EntityFrameworkCore; @@ -12,17 +13,25 @@ namespace LANCommander.Server.Tests; [TestCaseOrderer(DependencyOrderer.TypeName, DependencyOrderer.AssemblyName)] public abstract class BaseTest : IClassFixture, IDisposable { - protected readonly SDK.Client Client = ApplicationFixture.Instance.Client; + protected AuthenticationClient AuthenticationClient => ApplicationFixture.Instance.AuthenticationClient; + protected GameClient GameClient => ApplicationFixture.Instance.GameClient; + protected SaveClient SaveClient => ApplicationFixture.Instance.SaveClient; + protected TagClient TagClient => ApplicationFixture.Instance.TagClient; + protected readonly IServiceProvider ServiceProvider; - + private AsyncServiceScope? _scope; - + public BaseTest(ApplicationFixture fixture) { _scope = ApplicationFixture.Instance.ServiceProvider.CreateAsyncScope(); - + ServiceProvider = _scope?.ServiceProvider; } + + /// Authenticates against the in-memory server; the token is reused by the SDK clients. + protected Task AuthenticateAsync(string username, string password) + => ApplicationFixture.Instance.AuthenticateAsync(username, password); protected T GetService() => ServiceProvider.GetService(); diff --git a/LANCommander.Server.Tests/Client/AuthenticationTests.cs b/LANCommander.Server.Tests/Client/AuthenticationTests.cs index 62f4e904..4ae4c692 100644 --- a/LANCommander.Server.Tests/Client/AuthenticationTests.cs +++ b/LANCommander.Server.Tests/Client/AuthenticationTests.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Mvc.Testing; +using LANCommander.SDK.Extensions; using Shouldly; namespace LANCommander.Server.Tests.Client; @@ -16,8 +16,18 @@ public class AuthenticationTests : IClassFixture [Fact] public async Task PingShouldWork() { - var response = await _fixture.Client.PingAsync(); - - response.ShouldBeTrue(); + // ConnectionClient.PingAsync uses a static HttpClient that does real network I/O and so + // cannot reach the in-memory test server. Exercise the server's PingMiddleware directly + // through the in-memory handler instead, asserting the same X-Ping/X-Pong contract. + var pingId = Guid.NewGuid().ToString(); + + var request = new HttpRequestMessage(HttpMethod.Head, _fixture.ServerAddress); + request.Headers.Add("X-Ping", pingId); + + var response = await _fixture.HttpClient.SendAsync(request); + + response.IsSuccessStatusCode.ShouldBeTrue(); + response.Headers.Contains("X-Pong").ShouldBeTrue(); + response.Headers.GetValues("X-Pong").First().ShouldBe(pingId.FastReverse()); } } \ No newline at end of file diff --git a/LANCommander.Server.Tests/Data/VersioningTests.cs b/LANCommander.Server.Tests/Data/VersioningTests.cs index 966b7ddb..86dcc9e7 100644 --- a/LANCommander.Server.Tests/Data/VersioningTests.cs +++ b/LANCommander.Server.Tests/Data/VersioningTests.cs @@ -1,4 +1,3 @@ -using LANCommander.SDK.Services; using Shouldly; using LANCommander.Server.Services; @@ -12,13 +11,12 @@ public class VersioningTests(ApplicationFixture fixture) : BaseTest(fixture) { // Simple service that's not bound to change much var tagService = GetService(); - var authenticationClient = GetService(); - - var user = await EnsureAdminUserCreatedAsync(); - - await authenticationClient.AuthenticateAsync(TestConstants.AdminUserName, TestConstants.AdminInitialPassword); - var response = await Client.Tags.CreateAsync(new SDK.Models.Tag + var user = await EnsureAdminUserCreatedAsync(); + + await AuthenticateAsync(TestConstants.AdminUserName, TestConstants.AdminInitialPassword); + + var response = await TagClient.CreateAsync(new SDK.Models.Tag { Name = "Test Tag", }); diff --git a/LANCommander.Server.Tests/Endpoints/ThrottledStreamTests.cs b/LANCommander.Server.Tests/Endpoints/ThrottledStreamTests.cs new file mode 100644 index 00000000..032802c0 --- /dev/null +++ b/LANCommander.Server.Tests/Endpoints/ThrottledStreamTests.cs @@ -0,0 +1,60 @@ +using System.Diagnostics; +using LANCommander.Server.Endpoints; +using Shouldly; + +namespace LANCommander.Server.Tests.Endpoints; + +public class ThrottledStreamTests +{ + [Fact] + public async Task UnlimitedStreamReadsAllBytesWithoutDelay() + { + var data = new byte[256 * 1024]; + Random.Shared.NextBytes(data); + + using var inner = new MemoryStream(data); + await using var throttled = new ThrottledStream(inner, 0); + + var output = new MemoryStream(); + var stopwatch = Stopwatch.StartNew(); + await throttled.CopyToAsync(output, 16 * 1024); + stopwatch.Stop(); + + output.ToArray().ShouldBe(data); + stopwatch.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(1)); + } + + [Fact] + public async Task ThrottledStreamLimitsThroughput() + { + // 200 KB at 100 KB/s should take at least ~1 second (one full window beyond the first). + const int rate = 100 * 1024; + var data = new byte[200 * 1024]; + + using var inner = new MemoryStream(data); + await using var throttled = new ThrottledStream(inner, rate); + + var output = new MemoryStream(); + var stopwatch = Stopwatch.StartNew(); + await throttled.CopyToAsync(output, 16 * 1024); + stopwatch.Stop(); + + output.Length.ShouldBe(data.Length); + stopwatch.Elapsed.ShouldBeGreaterThan(TimeSpan.FromMilliseconds(800)); + } + + [Fact] + public void LengthAndSeekAreDelegatedToInner() + { + var data = new byte[1024]; + using var inner = new MemoryStream(data); + using var throttled = new ThrottledStream(inner, 1024); + + throttled.Length.ShouldBe(data.Length); + throttled.CanSeek.ShouldBeTrue(); + + throttled.Seek(100, SeekOrigin.Begin); + throttled.Position.ShouldBe(100); + inner.Position.ShouldBe(100); + } +} diff --git a/LANCommander.Server.Tests/Mocks/GitHubServiceMockFactory.cs b/LANCommander.Server.Tests/Mocks/GitHubServiceMockFactory.cs index b6faf438..e450047f 100644 --- a/LANCommander.Server.Tests/Mocks/GitHubServiceMockFactory.cs +++ b/LANCommander.Server.Tests/Mocks/GitHubServiceMockFactory.cs @@ -1,5 +1,6 @@ using LANCommander.Server.Services.Abstractions; using LANCommander.Server.Services.Models; +using LANCommander.Server.Settings.Enums; using Moq; using Octokit; using Semver; @@ -79,6 +80,20 @@ public static class GitHubServiceMockFactory return CreateRelease(version); }); + // Setup GetReleaseAsync (tag overload) — UpdateService resolves releases by string tag + // (e.g. "v1.0.0" or "nightly"); without this the mock returns null and callers NRE. + mock.Setup(x => x.GetReleaseAsync(It.IsAny())) + .ReturnsAsync((string tag) => + { + var trimmed = tag?.TrimStart('v'); + + var version = SemVersion.TryParse(trimmed, SemVersionStyles.Any, out var parsed) + ? parsed + : Version; + + return CreateRelease(version); + }); + // Setup GetReleasesAsync mock.Setup(x => x.GetReleasesAsync(It.IsAny())) .ReturnsAsync((int count) => diff --git a/LANCommander.Server.Tests/Mocks/VersionProviderMock.cs b/LANCommander.Server.Tests/Mocks/VersionProviderMock.cs index ec20e4b7..0c4ff0cf 100644 --- a/LANCommander.Server.Tests/Mocks/VersionProviderMock.cs +++ b/LANCommander.Server.Tests/Mocks/VersionProviderMock.cs @@ -1,5 +1,8 @@ using LANCommander.Server.Services.Abstractions; using LANCommander.Server.Services.Models; +using System; +using System.Linq; +using LANCommander.Server.Settings.Enums; using Semver; namespace LANCommander.Server.Tests.Mocks; @@ -15,7 +18,16 @@ public class VersionProviderMock : IVersionProvider public ReleaseChannel GetReleaseChannel(SemVersion version) { - throw new NotImplementedException(); + if (version.IsRelease) + return ReleaseChannel.Stable; + + if (version.IsPrerelease && version.PrereleaseIdentifiers.Any(pi => pi.Value == "nightly")) + return ReleaseChannel.Nightly; + + if (version.IsPrerelease) + return ReleaseChannel.Prerelease; + + throw new ArgumentException("Could not parse version number", nameof(version)); } public static void SetVersion(string version) diff --git a/LANCommander.Server.Tests/Services/ImporterTests.cs b/LANCommander.Server.Tests/Services/ImporterTests.cs index 5389dfc2..b721b620 100644 --- a/LANCommander.Server.Tests/Services/ImporterTests.cs +++ b/LANCommander.Server.Tests/Services/ImporterTests.cs @@ -1,7 +1,7 @@ -using LANCommander.Server.Services.Factories; using Action = LANCommander.SDK.Models.Manifest.Action; using LANCommander.SDK.Enums; using LANCommander.Server.ImportExport.Factories; +using LANCommander.Server.Services; namespace LANCommander.Server.Tests.Services; @@ -244,32 +244,23 @@ public class ImporterTests(ApplicationFixture fixture) : BaseTest(fixture) } }; - // Set the manifest on the import context + var gameService = GetService(); - // Prepare the import queue with all flags - var importFlags = ImportRecordFlags.Actions | ImportRecordFlags.Archives | ImportRecordFlags.Collections | - ImportRecordFlags.CustomFields | ImportRecordFlags.Developers | ImportRecordFlags.Engine | - ImportRecordFlags.Genres | ImportRecordFlags.Keys | ImportRecordFlags.Media | - ImportRecordFlags.MultiplayerModes | ImportRecordFlags.Platforms | ImportRecordFlags.PlaySessions | - ImportRecordFlags.Publishers | ImportRecordFlags.Saves | ImportRecordFlags.SavePaths | - ImportRecordFlags.Scripts | ImportRecordFlags.Tags; + // Queue the manifest's metadata records (developers, engine, genres, multiplayer modes, + // publishers, save paths, tags and the game itself) and run the import pipeline. This is + // the archive-free entry point used for in-memory imports. + await importContext.InitializeMetadataUpdateAsync(manifest); - await importContext.PrepareGameImportQueueAsync(manifest, importFlags); - - // Import the queue await importContext.ImportQueueAsync(); - // Assert that the game was imported successfully - Assert.NotNull(importContext.DataRecord); - Assert.IsType(importContext.DataRecord); - - var importedGame = (LANCommander.Server.Data.Models.Game)importContext.DataRecord; + // Assert that the game was imported successfully by reading it back from the service + var importedGame = await gameService.GetAsync(manifest.Id); + + Assert.NotNull(importedGame); Assert.Equal("Test Game", importedGame.Title); Assert.Equal("A comprehensive test game for import testing", importedGame.Description); - // Assert that all items were processed - // Note: The exact count may vary depending on how the importers handle the data - // We expect at least some items to be processed + // Assert that all queued items were processed Assert.True(importContext.Processed > 0, "At least some items should have been processed"); // Clean up diff --git a/LANCommander.Server.Tests/Services/SaveClientTests.cs b/LANCommander.Server.Tests/Services/SaveClientTests.cs index 4eecb9a3..1b1a9760 100644 --- a/LANCommander.Server.Tests/Services/SaveClientTests.cs +++ b/LANCommander.Server.Tests/Services/SaveClientTests.cs @@ -24,19 +24,19 @@ public class SaveClientTests(ApplicationFixture fixture) : BaseTest(fixture) { File.WriteAllText("test.txt", "Hello World!"); - using (var archive = ZipArchive.Create()) + using (var archive = ZipArchive.CreateArchive()) { archive.AddEntry("test.txt", "test.txt"); archive.SaveTo("test.zip", CompressionType.None); - + var fileInfo = new FileInfo("test.zip"); - + fileInfo.Length.ShouldBe(126); } using (Stream stream = File.OpenRead("test.zip")) - using (var reader = ReaderFactory.Open(stream)) + using (var reader = ReaderFactory.OpenReader(stream)) { while (reader.MoveToNextEntry()) { @@ -63,7 +63,7 @@ public class SaveClientTests(ApplicationFixture fixture) : BaseTest(fixture) File.WriteAllText("test.txt", "Hello World!"); using (var ms = new MemoryStream()) - using (var archive = ZipArchive.Create()) + using (var archive = ZipArchive.CreateArchive()) { archive.AddEntry("test.txt", "test.txt"); @@ -73,7 +73,7 @@ public class SaveClientTests(ApplicationFixture fixture) : BaseTest(fixture) ms.Length.ShouldBe(126); - using (var reader = ReaderFactory.Open(ms)) + using (var reader = ReaderFactory.OpenReader(ms)) { while (reader.MoveToNextEntry()) { @@ -103,9 +103,9 @@ public class SaveClientTests(ApplicationFixture fixture) : BaseTest(fixture) var saveService = GetService(); var user = await EnsureAdminUserCreatedAsync(); - - await Client.AuthenticateAsync(TestConstants.AdminUserName, TestConstants.AdminInitialPassword); - + + await AuthenticateAsync(TestConstants.AdminUserName, TestConstants.AdminInitialPassword); + var installDirectory = GetTemporaryDirectory(); var tempPath = await EnsureStorageLocationsExistAsync(); @@ -128,10 +128,10 @@ public class SaveClientTests(ApplicationFixture fixture) : BaseTest(fixture) game = await gameService.AddAsync(game); // Mock game install directory - var sdkGame = await gameClient.GetAsync(game.Id); + var sdkGame = await GameClient.GetAsync(game.Id); - var gameInstallDirectory = await gameClient.GetInstallDirectory(sdkGame, installDirectory); - var manifest = gameClient.GetManifest(game.Id); + var gameInstallDirectory = await GameClient.GetInstallDirectory(sdkGame, installDirectory); + var manifest = await GameClient.GetManifestAsync(game.Id); Directory.CreateDirectory(Path.Combine(gameInstallDirectory, ".lancommander")); Directory.CreateDirectory(Path.Combine(gameInstallDirectory, "save")); @@ -160,7 +160,7 @@ public class SaveClientTests(ApplicationFixture fixture) : BaseTest(fixture) var stream = await savePacker.PackAsync(); - using (var reader = ReaderFactory.Open(stream, new ReaderOptions() + using (var reader = ReaderFactory.OpenReader(stream, new ReaderOptions() { LeaveStreamOpen = true, })) @@ -184,7 +184,7 @@ public class SaveClientTests(ApplicationFixture fixture) : BaseTest(fixture) packedSize = stream.Length; - uploadedSave = await Client.Saves.UploadAsync(stream, manifest); + uploadedSave = await SaveClient.UploadAsync(stream, manifest); } #endregion @@ -209,7 +209,7 @@ public class SaveClientTests(ApplicationFixture fixture) : BaseTest(fixture) // Check contents of file using (var fs = File.OpenRead(uploadedSavePath)) - using (var reader = ReaderFactory.Open(fs, new ReaderOptions() + using (var reader = ReaderFactory.OpenReader(fs, new ReaderOptions() { LeaveStreamOpen = true, })) diff --git a/LANCommander.Server.Tests/Services/UpdateServiceTests.cs b/LANCommander.Server.Tests/Services/UpdateServiceTests.cs index d6f4d68a..24bf646c 100644 --- a/LANCommander.Server.Tests/Services/UpdateServiceTests.cs +++ b/LANCommander.Server.Tests/Services/UpdateServiceTests.cs @@ -1,6 +1,7 @@ using LANCommander.Server.Data.Models; using LANCommander.Server.Services; using LANCommander.Server.Services.Models; +using LANCommander.Server.Settings.Enums; using LANCommander.Server.Tests.Mocks; using Moq; using Shouldly; diff --git a/LANCommander.Server.Tests/Services/UserLimitsResolveTests.cs b/LANCommander.Server.Tests/Services/UserLimitsResolveTests.cs new file mode 100644 index 00000000..65e1cc87 --- /dev/null +++ b/LANCommander.Server.Tests/Services/UserLimitsResolveTests.cs @@ -0,0 +1,94 @@ +using LANCommander.Server.Services; +using Shouldly; + +namespace LANCommander.Server.Tests.Services; + +public class UserLimitsResolveTests +{ + [Theory] + [InlineData(50, 50)] // explicit override wins over role + [InlineData(0, 0)] // override of 0 (unlimited) wins + [InlineData(5, 5)] // override below role values still wins + public void UserOverrideTakesPrecedence(int? userOverride, int expected) + { + var result = UserService.Resolve(userOverride, new int?[] { 10, 20 }); + + result.ShouldBe(expected); + } + + [Fact] + public void NoOverrideUsesLowestNonZeroRoleValue() + { + var result = UserService.Resolve(null, new int?[] { 20, 5, 50 }); + + result.ShouldBe(5); + } + + [Fact] + public void NullAndZeroRoleValuesAreIgnoredWhenResolving() + { + // null = not configured, 0 = explicit unlimited; both ignored so the only real limit (15) wins. + var result = UserService.Resolve(null, new int?[] { null, 0, 15 }); + + result.ShouldBe(15); + } + + [Fact] + public void AllRolesUnsetMeansUnlimited() + { + var result = UserService.Resolve(null, new int?[] { null, null }); + + result.ShouldBe(0); + } + + [Fact] + public void AllRolesExplicitlyUnlimitedMeansUnlimited() + { + var result = UserService.Resolve(null, new int?[] { 0, 0 }); + + result.ShouldBe(0); + } + + [Fact] + public void NoRolesAndNoOverrideMeansUnlimited() + { + var result = UserService.Resolve(null, Array.Empty()); + + result.ShouldBe(0); + } + + [Theory] + [InlineData(true, true)] // override true (enabled) wins even though a role disables + [InlineData(false, false)] // override false (disabled) wins even though roles enable + public void BoolUserOverrideTakesPrecedence(bool? userOverride, bool expected) + { + var result = UserService.Resolve(userOverride, new bool?[] { true, false }); + + result.ShouldBe(expected); + } + + [Fact] + public void BoolAnyRoleDisabledMeansDisabled() + { + // false = explicitly disabled; most restrictive wins so the effective permission is disabled. + var result = UserService.Resolve((bool?)null, new bool?[] { true, false, null }); + + result.ShouldBeFalse(); + } + + [Fact] + public void BoolAllRolesUnsetOrEnabledMeansEnabled() + { + var result = UserService.Resolve((bool?)null, new bool?[] { null, true }); + + result.ShouldBeTrue(); + } + + [Fact] + public void BoolNoRolesAndNoOverrideMeansEnabled() + { + var result = UserService.Resolve((bool?)null, Array.Empty()); + + result.ShouldBeTrue(); + } +} diff --git a/LANCommander.Server.Tests/Services/UserServiceTests.cs b/LANCommander.Server.Tests/Services/UserServiceTests.cs index f4abcb40..64c5f741 100644 --- a/LANCommander.Server.Tests/Services/UserServiceTests.cs +++ b/LANCommander.Server.Tests/Services/UserServiceTests.cs @@ -23,11 +23,15 @@ public class UserServiceTests(ApplicationFixture fixture) : BaseTest(fixture) var userService = GetService(); var result = await userService.ChangePassword(TestConstants.AdminUserName, TestConstants.AdminInitialPassword, TestConstants.AdminPassword); - + result.Succeeded.ShouldBeTrue(); - + var validPassword = await userService.CheckPassword(TestConstants.AdminUserName, TestConstants.AdminPassword); - + validPassword.ShouldBeTrue(); + + // The admin account is shared across the in-memory test database, so restore the original + // password to avoid breaking other tests that authenticate with AdminInitialPassword. + await userService.ChangePassword(TestConstants.AdminUserName, TestConstants.AdminPassword, TestConstants.AdminInitialPassword); } } \ No newline at end of file