Fix tests, add tests for user limits
This commit is contained in:
parent
fa1559c367
commit
69f8447cae
13 changed files with 331 additions and 59 deletions
|
|
@ -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<TProgram>
|
|||
|
||||
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<IPostConfigureOptions<JwtBearerOptions>, AlignJwtSigningKey>();
|
||||
#endregion
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class AlignJwtSigningKey(SettingsProvider<ServerSettings> settingsProvider)
|
||||
: IPostConfigureOptions<JwtBearerOptions>
|
||||
{
|
||||
public void PostConfigure(string? name, JwtBearerOptions options)
|
||||
{
|
||||
var secret = settingsProvider.CurrentValue.Server.Authentication.TokenSecret;
|
||||
|
||||
options.TokenValidationParameters.IssuerSigningKey =
|
||||
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Program>
|
||||
{
|
||||
public static ApplicationFixture Instance;
|
||||
|
||||
public SDK.Client Client { get; set; }
|
||||
|
||||
/// <summary>Server-side service provider (the in-memory app under test).</summary>
|
||||
public IServiceProvider ServiceProvider { get; set; }
|
||||
|
||||
/// <summary>Client-side SDK service provider, wired to talk to the in-memory server.</summary>
|
||||
public IServiceProvider ClientServiceProvider { get; set; }
|
||||
|
||||
/// <summary>HttpClient whose handler routes to the in-memory server.</summary>
|
||||
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<Program> 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<SDK.Models.Settings>().Configure(_ => { });
|
||||
services.AddSingleton<IServerConfigurationRefresher>(NoopRefresher.Instance);
|
||||
services.AddLANCommanderClient<SDK.Models.Settings>();
|
||||
services.AddSingleton(HttpClient);
|
||||
|
||||
ClientServiceProvider = services.BuildServiceProvider();
|
||||
|
||||
ClientServiceProvider.GetRequiredService<IServerAddressProvider>().SetServerAddress(ServerAddress);
|
||||
|
||||
AuthenticationClient = ClientServiceProvider.GetRequiredService<AuthenticationClient>();
|
||||
GameClient = ClientServiceProvider.GetRequiredService<GameClient>();
|
||||
SaveClient = ClientServiceProvider.GetRequiredService<SaveClient>();
|
||||
TagClient = ClientServiceProvider.GetRequiredService<TagClient>();
|
||||
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
/// <summary>Authenticates against the in-memory server and stores the token for subsequent client calls.</summary>
|
||||
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<ApplicationFactory<Program>>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ApplicationFixture>, 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;
|
||||
}
|
||||
|
||||
/// <summary>Authenticates against the in-memory server; the token is reused by the SDK clients.</summary>
|
||||
protected Task AuthenticateAsync(string username, string password)
|
||||
=> ApplicationFixture.Instance.AuthenticateAsync(username, password);
|
||||
|
||||
protected T GetService<T>() => ServiceProvider.GetService<T>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ApplicationFixture>
|
|||
[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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TagService>();
|
||||
var authenticationClient = GetService<AuthenticationClient>();
|
||||
|
||||
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",
|
||||
});
|
||||
|
|
|
|||
60
LANCommander.Server.Tests/Endpoints/ThrottledStreamTests.cs
Normal file
60
LANCommander.Server.Tests/Endpoints/ThrottledStreamTests.cs
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string>()))
|
||||
.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<int>()))
|
||||
.ReturnsAsync((int count) =>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<GameService>();
|
||||
|
||||
// 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<LANCommander.Server.Data.Models.Game>(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
|
||||
|
|
|
|||
|
|
@ -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<GameSaveService>();
|
||||
|
||||
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,
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
94
LANCommander.Server.Tests/Services/UserLimitsResolveTests.cs
Normal file
94
LANCommander.Server.Tests/Services/UserLimitsResolveTests.cs
Normal file
|
|
@ -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<int?>());
|
||||
|
||||
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<bool?>());
|
||||
|
||||
result.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
|
|
@ -23,11 +23,15 @@ public class UserServiceTests(ApplicationFixture fixture) : BaseTest(fixture)
|
|||
var userService = GetService<UserService>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue