Files
Ludarium-Public/tests/Ludarium.IntegrationTests/NativeBrowserPlayServiceTests.cs
T
Ludarium release export df869819ce
Public source validation / source (push) Successful in 2m16s
Publish Ludarium source
2026-09-03 02:08:58 +02:00

214 lines
12 KiB
C#

using Ludarium.Application;
using Ludarium.Domain;
using Ludarium.Infrastructure;
using System.Reflection;
namespace Ludarium.IntegrationTests;
public sealed class NativeBrowserPlayServiceTests
{
[Fact]
public async Task ExactReadOnlyAllowlistedMappingStartsNativeSessionAndCancelsIt()
{
var gameId = Guid.NewGuid();
var store = new MemoryPlayStore(Candidate(gameId));
var service = Service(store);
var capability = await service.GetCapabilityAsync(gameId, CancellationToken.None);
var launch = await service.StartAsync(gameId, "tester", CancellationToken.None);
Assert.True(capability.Available);
Assert.Equal("Ludarium EmulatorJS", capability.Emulator);
Assert.StartsWith("/player.html?session=", launch.Session.LaunchUrl, StringComparison.Ordinal);
Assert.Contains("#token=", launch.Session.LaunchUrl, StringComparison.Ordinal);
Assert.DoesNotContain("firmware=", launch.Session.LaunchUrl, StringComparison.Ordinal);
Assert.DoesNotContain(launch.Token, store.Sessions[launch.Session.Id].LaunchUrl, StringComparison.Ordinal);
Assert.Equal(64, launch.Token.Length);
Assert.Null(await service.AuthorizeAsync(launch.Session.Id, "wrong-token", CancellationToken.None));
Assert.Equal(launch.Session.Id, (await service.AuthorizeAsync(launch.Session.Id, launch.Token,
CancellationToken.None))?.Id);
await service.CancelAsync(launch.Session.Id, "tester", CancellationToken.None);
Assert.Equal(PlaySessionState.Cancelled, store.Sessions[launch.Session.Id].State);
}
[Fact]
public async Task SelectedPersonalPsxBiosIsAdvertisedWithoutExposingItsIdentity()
{
var gameId = Guid.NewGuid();
var store = new MemoryPlayStore(Candidate(gameId, "psx", "roms/psx/synthetic.chd"));
var firmware = new MemoryFirmwareStore(new FirmwareAsset(Guid.NewGuid(), "psx", "bios",
FirmwareAssetKind.Bios, "personal.bin", 512, new string('f', 64), "private/location", true,
DateTimeOffset.UtcNow, DateTimeOffset.UtcNow));
var service = new NativeBrowserPlayService(store, null!, null!, true, firmwareStore: firmware);
var launch = await service.StartAsync(gameId, "tester", CancellationToken.None);
Assert.Contains("&firmware=1#token=", launch.Session.LaunchUrl, StringComparison.Ordinal);
Assert.DoesNotContain("personal.bin", launch.Session.LaunchUrl, StringComparison.Ordinal);
Assert.DoesNotContain("private/location", launch.Session.LaunchUrl, StringComparison.Ordinal);
}
[Fact]
public async Task Ps2UsesTheNativePlayJsPageWithoutDeliveringTheStoredBios()
{
var gameId = Guid.NewGuid();
var store = new MemoryPlayStore(Candidate(gameId, "ps2", "roms/ps2/synthetic.iso"));
var firmware = new MemoryFirmwareStore(new FirmwareAsset(Guid.NewGuid(), "ps2", "bios",
FirmwareAssetKind.Bios, "personal.bin", 512, new string('f', 64), "private/location", true,
DateTimeOffset.UtcNow, DateTimeOffset.UtcNow));
var service = new NativeBrowserPlayService(store, null!, null!, true, firmwareStore: firmware);
var capability = await service.GetCapabilityAsync(gameId, CancellationToken.None);
var launch = await service.StartAsync(gameId, "tester", CancellationToken.None);
Assert.True(capability.Available);
Assert.Equal("Ludarium Play!.js", capability.Emulator);
Assert.Equal("playjs", capability.Core);
Assert.False(capability.AutomaticRestore);
Assert.StartsWith("/ps2-player.html?session=", launch.Session.LaunchUrl, StringComparison.Ordinal);
Assert.DoesNotContain("firmware=", launch.Session.LaunchUrl, StringComparison.Ordinal);
Assert.DoesNotContain("personal.bin", launch.Session.LaunchUrl, StringComparison.Ordinal);
}
[Fact]
public async Task N64UsesItsDedicatedWasmPlayerAndVerifiedRestoreContract()
{
var gameId = Guid.NewGuid();
var store = new MemoryPlayStore(Candidate(gameId, "n64", "roms/n64/synthetic.v64"));
var service = new NativeBrowserPlayService(store, null!, null!, true);
var capability = await service.GetCapabilityAsync(gameId, CancellationToken.None);
var launch = await service.StartAsync(gameId, "tester", CancellationToken.None);
Assert.True(capability.Available);
Assert.Equal("Ludarium N64Wasm", capability.Emulator);
Assert.Equal("n64wasm", capability.Core);
Assert.True(capability.AutomaticRestore);
Assert.StartsWith("/n64-player.html?session=", launch.Session.LaunchUrl, StringComparison.Ordinal);
Assert.Contains("&core=n64wasm", launch.Session.LaunchUrl, StringComparison.Ordinal);
}
[Fact]
public async Task MissingGameAndMissingBundledRuntimeFailClosed()
{
var missing = await Service(new MemoryPlayStore(null)).GetCapabilityAsync(Guid.NewGuid(), CancellationToken.None);
var gameId = Guid.NewGuid();
var unavailable = await new NativeBrowserPlayService(new MemoryPlayStore(Candidate(gameId)), null!, null!, false)
.GetCapabilityAsync(gameId, CancellationToken.None);
Assert.Equal(BrowserPlayState.MissingRom, missing.State);
Assert.Equal(BrowserPlayState.EmulatorUnavailable, unavailable.State);
}
[Fact]
public async Task MissingScanDerivedN64ZipFailsClosedWithoutBreakingCapabilityChecks()
{
var rootPath = Path.Combine(Path.GetTempPath(), "ludarium-browser-play-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(rootPath);
try
{
var gameId = Guid.NewGuid();
var candidate = Candidate(gameId, "n64", "roms/n64/missing.zip");
var library = new LibraryRoot(Guid.NewGuid(), "Read-only games", rootPath, LibraryKind.Rom, true,
HashPolicy.Sha256, false, 1, true, true, true, true);
var artifact = new Artifact(candidate.ArtifactId, library.Id, candidate.RelativePath, candidate.Size,
DateTimeOffset.UtcNow, ArtifactState.Present, MediaType.Rom, Confidence.High, null,
DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, null, "n64");
var catalog = DispatchProxy.Create<ILudariumStore, ArtifactDetailsStoreProxy>();
((ArtifactDetailsStoreProxy)(object)catalog).Details = new(artifact, library, [], [], [], [], []);
var service = new NativeBrowserPlayService(new MemoryPlayStore(candidate), catalog,
new ReadOnlyLibraryFileSystem(), true);
var capability = await service.GetCapabilityAsync(gameId, CancellationToken.None);
var startError = await Assert.ThrowsAsync<ResourceConflictException>(() =>
service.StartAsync(gameId, "tester", CancellationToken.None));
Assert.False(capability.Available);
Assert.Equal(BrowserPlayState.MissingRom, capability.State);
Assert.Contains("no longer present or readable", capability.Message, StringComparison.OrdinalIgnoreCase);
Assert.Contains("no longer present or readable", startError.Message, StringComparison.OrdinalIgnoreCase);
}
finally
{
Directory.Delete(rootPath, true);
}
}
[Fact]
public async Task ConcurrentSessionLimitFailsClosed()
{
var gameId = Guid.NewGuid();
var store = new MemoryPlayStore(Candidate(gameId));
var now = DateTimeOffset.UtcNow;
for (var index = 0; index < 2; index++)
{
var session = new BrowserPlaySession(Guid.NewGuid(), gameId, Guid.NewGuid(), PlaySessionState.Active,
"nes", "Ludarium EmulatorJS", "fceumm", "/player.html", "redacted", now, now.AddMinutes(30));
store.Sessions[session.Id] = session;
}
var error = await Assert.ThrowsAsync<ResourceConflictException>(() =>
Service(store).StartAsync(gameId, "tester", CancellationToken.None));
Assert.Contains("capacity", error.Message, StringComparison.OrdinalIgnoreCase);
Assert.Equal(2, store.Sessions.Count);
}
private static NativeBrowserPlayService Service(MemoryPlayStore store) => new(store, null!, null!, true);
private static BrowserPlayCandidate Candidate(Guid gameId, string platform = "nes",
string relativePath = "roms/nes/synthetic.nes") =>
new(gameId, Guid.NewGuid(), "Synthetic", platform, relativePath, 16, null, true);
private sealed class MemoryFirmwareStore(FirmwareAsset? selected) : IFirmwareStore
{
public Task<IReadOnlyList<FirmwareAsset>> ListFirmwareAssetsAsync(CancellationToken cancellationToken) =>
Task.FromResult<IReadOnlyList<FirmwareAsset>>(selected is null ? [] : [selected]);
public Task<FirmwareAsset?> GetFirmwareAssetAsync(Guid id, CancellationToken cancellationToken) =>
Task.FromResult(selected?.Id == id ? selected : null);
public Task<FirmwareAsset?> GetSelectedFirmwareAssetAsync(string platform, string slot,
CancellationToken cancellationToken) => Task.FromResult(selected is not null && selected.Selected &&
selected.Platform.Equals(platform, StringComparison.OrdinalIgnoreCase) &&
selected.Slot.Equals(slot, StringComparison.OrdinalIgnoreCase) ? selected : null);
public Task SaveFirmwareAssetAsync(FirmwareAsset asset, string actor, CancellationToken cancellationToken) =>
Task.CompletedTask;
public Task<IReadOnlyList<FirmwareAsset>> DeleteFirmwareAssetAsync(Guid id, long expectedVersion,
string actor, CancellationToken cancellationToken) =>
Task.FromResult<IReadOnlyList<FirmwareAsset>>([]);
}
public class ArtifactDetailsStoreProxy : DispatchProxy
{
public ArtifactDetails? Details { get; set; }
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) => targetMethod?.Name switch
{
nameof(ILudariumStore.GetArtifactDetailsAsync) => Task.FromResult(Details),
_ => throw new NotSupportedException(targetMethod?.Name)
};
}
private sealed class MemoryPlayStore(BrowserPlayCandidate? candidate) : IBrowserPlayStore
{
public Dictionary<Guid, BrowserPlaySession> Sessions { get; } = [];
public Task<BrowserPlayCandidate?> GetBrowserPlayCandidateAsync(Guid gameId, CancellationToken cancellationToken) =>
Task.FromResult(candidate?.GameId == gameId ? candidate : null);
public Task<int> CountActiveBrowserPlaySessionsAsync(DateTimeOffset now, CancellationToken cancellationToken) =>
Task.FromResult(Sessions.Values.Count(session => session.ExpiresAt > now && session.State is
(PlaySessionState.Starting or PlaySessionState.Ready or PlaySessionState.Active)));
public Task SaveBrowserPlaySessionAsync(BrowserPlaySession session, string actor, CancellationToken cancellationToken)
{
Sessions[session.Id] = session;
return Task.CompletedTask;
}
public Task<BrowserPlaySession?> GetBrowserPlaySessionAsync(Guid sessionId, CancellationToken cancellationToken) =>
Task.FromResult(Sessions.GetValueOrDefault(sessionId));
public Task EndBrowserPlaySessionAsync(Guid sessionId, PlaySessionState state, string actor,
string? errorCode, string? errorMessage, CancellationToken cancellationToken)
{
if (Sessions.TryGetValue(sessionId, out var session)) Sessions[sessionId] = session with
{ State = state, ErrorCode = errorCode, ErrorMessage = errorMessage, EndedAt = DateTimeOffset.UtcNow };
return Task.CompletedTask;
}
}
}