275 lines
13 KiB
C#
275 lines
13 KiB
C#
using Ludarium.Application;
|
|
using Ludarium.Domain;
|
|
|
|
namespace Ludarium.UnitTests;
|
|
|
|
public sealed class BrowserPlayPolicyTests
|
|
{
|
|
[Fact]
|
|
public void DeterministicNesFixtureIsAllowlistedOnlyWhenSourceIsReadOnly()
|
|
{
|
|
var fixture = Convert.FromBase64String(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "fixtures", "minimal-ines.base64")).Trim());
|
|
Assert.Equal([0x4e, 0x45, 0x53, 0x1a], fixture[..4]);
|
|
var candidate = Candidate("nes", "roms/nes/synthetic.nes", fixture.Length, readOnly: true);
|
|
|
|
var result = BrowserPlayPolicy.Evaluate(candidate, configured: true, emulatorHealthy: true, DateTimeOffset.UnixEpoch);
|
|
|
|
Assert.True(result.Available);
|
|
Assert.Equal(BrowserPlayState.Available, result.State);
|
|
Assert.Equal("fceumm", result.Core);
|
|
Assert.True(result.AutomaticRestore);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("pc", "PC/setup.exe", BrowserPlayState.UnsupportedPlatform)]
|
|
[InlineData("nes", "roms/nes/setup.exe", BrowserPlayState.UnsupportedFormat)]
|
|
[InlineData("nes", "roms/nes/launch.bat", BrowserPlayState.UnsupportedFormat)]
|
|
[InlineData("nes", "roms/nes/launch.ps1", BrowserPlayState.UnsupportedFormat)]
|
|
[InlineData("nes", "roms/nes/archive.zip", BrowserPlayState.UnsupportedFormat)]
|
|
[InlineData("nes", "roms/nes/../../private/game.nes", BrowserPlayState.UnsupportedFormat)]
|
|
[InlineData("nes", "C:\\private\\game.nes", BrowserPlayState.UnsupportedFormat)]
|
|
[InlineData("nes", "/private/game.nes", BrowserPlayState.UnsupportedFormat)]
|
|
public void NativeExecutableScriptAndNonAllowlistedContentFailClosed(string platform, string path, BrowserPlayState state)
|
|
{
|
|
var result = BrowserPlayPolicy.Evaluate(Candidate(platform, path, 64, true), true, true, DateTimeOffset.UnixEpoch);
|
|
Assert.False(result.Available);
|
|
Assert.Equal(state, result.State);
|
|
}
|
|
|
|
[Fact]
|
|
public void WritableMissingAndOversizedSourcesFailClosed()
|
|
{
|
|
Assert.Equal(BrowserPlayState.Disabled, BrowserPlayPolicy.Evaluate(Candidate("nes", "game.nes", 16, false), true, true, DateTimeOffset.UnixEpoch).State);
|
|
Assert.Equal("fceumm", BrowserPlayPolicy.Evaluate(Candidate("nes", "game.nes", 16, true), false, false, DateTimeOffset.UnixEpoch).Core);
|
|
Assert.Equal(BrowserPlayState.MissingRom, BrowserPlayPolicy.Evaluate(null, true, true, DateTimeOffset.UnixEpoch).State);
|
|
Assert.Equal(BrowserPlayState.UnsupportedFormat, BrowserPlayPolicy.Evaluate(Candidate("nes", "game.nes", BrowserPlayPolicy.MaximumRomBytes + 1, true), true, true, DateTimeOffset.UnixEpoch).State);
|
|
}
|
|
|
|
[Fact]
|
|
public void SafeNameCannotExposeAStoredPath()
|
|
{
|
|
Assert.Equal("game.nes", BrowserPlayPolicy.SafeFileName("roms/nes/../../private/game.nes"));
|
|
Assert.Equal("game.nes", BrowserPlayPolicy.SafeFileName("roms\\nes\\game.nes"));
|
|
}
|
|
|
|
[Fact]
|
|
public void FixtureQualifiedCoreClaimsVerifiedAutomaticRestore()
|
|
{
|
|
var result = BrowserPlayPolicy.Evaluate(Candidate("gba", "roms/gba/synthetic.gba", 64, true),
|
|
true, true, DateTimeOffset.UnixEpoch);
|
|
|
|
Assert.True(result.Available);
|
|
Assert.Equal(BrowserPlayState.Available, result.State);
|
|
Assert.Equal("mgba", result.Core);
|
|
Assert.True(result.AutomaticRestore);
|
|
}
|
|
|
|
[Fact]
|
|
public void N64ZipContainerProceedsOnlyToTheServiceLevelContentValidation()
|
|
{
|
|
var result = BrowserPlayPolicy.Evaluate(Candidate("n64", "roms/n64/game.zip", 1024, true),
|
|
true, true, DateTimeOffset.UnixEpoch);
|
|
|
|
Assert.True(result.Available);
|
|
Assert.Equal("n64wasm", result.Core);
|
|
}
|
|
|
|
[Fact]
|
|
public void Ps2UsesItsFixtureProvenDiscLimitWithoutClaimingStateRestore()
|
|
{
|
|
var belowLimit = BrowserPlayPolicy.Evaluate(
|
|
Candidate("ps2", "roms/ps2/personal.iso", BrowserPlayPolicy.MaximumRomBytes + 1, true),
|
|
true, true, DateTimeOffset.UnixEpoch);
|
|
var aboveLimit = BrowserPlayPolicy.Evaluate(
|
|
Candidate("ps2", "roms/ps2/personal.iso", 8L * 1024 * 1024 * 1024 + 1, true),
|
|
true, true, DateTimeOffset.UnixEpoch);
|
|
|
|
Assert.True(belowLimit.Available);
|
|
Assert.Equal("playjs", belowLimit.Core);
|
|
Assert.Equal("Ludarium Play!.js", belowLimit.Emulator);
|
|
Assert.False(belowLimit.AutomaticRestore);
|
|
Assert.Equal(BrowserPlayState.UnsupportedFormat, aboveLimit.State);
|
|
}
|
|
|
|
[Fact]
|
|
public void EveryAdvertisedCoreHasAReleaseFixture()
|
|
{
|
|
var qualified = BrowserPlayPolicy.DescribePlatforms().Where(platform => platform.ReleaseQualified).ToArray();
|
|
|
|
Assert.Equal(13, qualified.Length);
|
|
Assert.Contains(qualified, platform => platform.Platform == "nes" && platform.Core == "fceumm");
|
|
Assert.Contains(qualified, platform => platform.Platform == "snes" && platform.Core == "snes9x");
|
|
Assert.Contains(qualified, platform => platform.Platform == "gb" && platform.Core == "gambatte");
|
|
Assert.Contains(qualified, platform => platform.Platform == "gbc" && platform.Core == "gambatte");
|
|
Assert.Contains(qualified, platform => platform.Platform == "gba" && platform.Core == "mgba");
|
|
Assert.Contains(qualified, platform => platform.Platform == "nds" && platform.Core == "melonds");
|
|
Assert.Contains(qualified, platform => platform.Platform == "psp" && platform.Core == "ppsspp");
|
|
Assert.Contains(qualified, platform => platform.Platform == "psx" && platform.Core == "pcsx_rearmed");
|
|
Assert.Contains(qualified, platform => platform.Platform == "ps2" && platform.Core == "playjs" &&
|
|
platform.MaximumBytes == 8L * 1024 * 1024 * 1024);
|
|
Assert.Contains(qualified, platform => platform.Platform == "n64" && platform.Core == "n64wasm" &&
|
|
platform.MaximumBytes == BrowserPlayPolicy.MaximumRomBytes);
|
|
foreach (var platform in new[] { "genesis", "mastersystem", "gamegear" })
|
|
Assert.Contains(qualified, entry => entry.Platform == platform && entry.Core == "genesis_plus_gx");
|
|
}
|
|
|
|
private static BrowserPlayCandidate Candidate(string platform, string path, long size, bool readOnly) =>
|
|
new(Guid.NewGuid(), Guid.NewGuid(), "Synthetic", platform, path, size, null, readOnly);
|
|
[Fact]
|
|
public void ACueSheetWinsOverTheTracksItDescribes()
|
|
{
|
|
var gameId = Guid.NewGuid();
|
|
var sheet = Candidate(gameId, "roms/psx/Game/Game.cue", 512);
|
|
var track = Candidate(gameId, "roms/psx/Game/Game (Track 1).bin", 700_000_000);
|
|
var audio = Candidate(gameId, "roms/psx/Game/Game (Track 2).bin", 40_000_000);
|
|
|
|
// Without this the tracks make every multi-track disc look ambiguous and unplayable.
|
|
Assert.Same(sheet, BrowserPlayPolicy.SelectCandidate([track, sheet, audio]));
|
|
Assert.Same(track, BrowserPlayPolicy.SelectCandidate([track]));
|
|
}
|
|
|
|
[Fact]
|
|
public void APlaylistWinsOverTheDiscsAndTracksItNames()
|
|
{
|
|
var gameId = Guid.NewGuid();
|
|
var playlist = Candidate(gameId, "roms/psx/Game/Game.m3u", 64);
|
|
var firstDisc = Candidate(gameId, "roms/psx/Game/Game (Disc 1).cue", 512);
|
|
var secondDisc = Candidate(gameId, "roms/psx/Game/Game (Disc 2).cue", 512);
|
|
var track = Candidate(gameId, "roms/psx/Game/Game (Disc 1) (Track 1).bin", 700_000_000);
|
|
|
|
// Descriptors outrank what they describe, most general first.
|
|
Assert.Same(playlist, BrowserPlayPolicy.SelectCandidate([firstDisc, track, playlist, secondDisc]));
|
|
Assert.Null(BrowserPlayPolicy.SelectCandidate([firstDisc, secondDisc, track]));
|
|
}
|
|
|
|
[Fact]
|
|
public void AMultiDiscGameIsDeliverableButNotYetQualified()
|
|
{
|
|
var psx = BrowserPlayPolicy.GetPlatform("psx")!;
|
|
Assert.True(psx.Accepts(".m3u"));
|
|
Assert.False(psx.IsQualified(".m3u"));
|
|
Assert.False(BrowserPlayPolicy.GetPlatform("nes")!.Accepts(".m3u"));
|
|
}
|
|
|
|
[Fact]
|
|
public void TwoSheetsOrTwoDiscsStayUnresolvable()
|
|
{
|
|
var gameId = Guid.NewGuid();
|
|
Assert.Null(BrowserPlayPolicy.SelectCandidate([
|
|
Candidate(gameId, "roms/psx/Game/Disc 1.cue", 512),
|
|
Candidate(gameId, "roms/psx/Game/Disc 2.cue", 512)]));
|
|
Assert.Null(BrowserPlayPolicy.SelectCandidate([
|
|
Candidate(gameId, "roms/psx/Game/Disc 1.chd", 500_000_000),
|
|
Candidate(gameId, "roms/psx/Game/Disc 2.chd", 500_000_000)]));
|
|
Assert.Null(BrowserPlayPolicy.SelectCandidate([]));
|
|
}
|
|
|
|
[Fact]
|
|
public void SegaPlatformsAreReleaseQualifiedByTheirDeterministicFixtures()
|
|
{
|
|
foreach (var platform in new[] { "genesis", "mastersystem", "gamegear" })
|
|
{
|
|
var mapping = BrowserPlayPolicy.GetPlatform(platform);
|
|
Assert.NotNull(mapping);
|
|
Assert.Equal("genesis_plus_gx", mapping.Core);
|
|
// Rule 10: each of these completed the live core gate on its own pinned fixture, which
|
|
// booted genesis_plus_gx, captured a savestate and restored it into a relaunched session.
|
|
Assert.True(mapping.ReleaseQualified);
|
|
Assert.Contains("genesis_plus_gx", BrowserPlayPolicy.RequiredCores);
|
|
}
|
|
|
|
// The savestate half of that evidence is what lets a session restore without being asked.
|
|
Assert.True(BrowserGameDataPolicy.SupportsAutomaticRestore("genesis_plus_gx"));
|
|
}
|
|
|
|
[Fact]
|
|
public void EveryMappedCoreIsRequiredExceptLudariumsOwnPlayers()
|
|
{
|
|
Assert.DoesNotContain("playjs", BrowserPlayPolicy.RequiredCores);
|
|
Assert.DoesNotContain("n64wasm", BrowserPlayPolicy.RequiredCores);
|
|
foreach (var platform in BrowserPlayPolicy.DescribePlatforms())
|
|
if (platform.Core is not "playjs" and not "n64wasm")
|
|
Assert.Contains(platform.Core, BrowserPlayPolicy.RequiredCores);
|
|
}
|
|
|
|
[Fact]
|
|
public void ACueSetIsDeliverableForPlayStationButNotForPlayJs()
|
|
{
|
|
var psx = BrowserPlayPolicy.GetPlatform("psx")!;
|
|
var ps2 = BrowserPlayPolicy.GetPlatform("ps2")!;
|
|
|
|
// EmulatorJS mounts the streamed archive; Play!.js boots a raw disc image and cannot.
|
|
Assert.True(psx.Accepts(".cue"));
|
|
Assert.False(ps2.Accepts(".cue"));
|
|
Assert.False(BrowserPlayPolicy.GetPlatform("nes")!.Accepts(".cue"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ADeliverableButUnqualifiedFormatFailsClosedWithAnExactReason()
|
|
{
|
|
var gameId = Guid.NewGuid();
|
|
var qualified = BrowserPlayPolicy.Evaluate(Candidate(gameId, "roms/psx/Game/Game.chd", 500_000_000),
|
|
true, true, DateTimeOffset.UnixEpoch);
|
|
var candidate = BrowserPlayPolicy.Evaluate(Candidate(gameId, "roms/psx/Game/Game.cue", 512),
|
|
true, true, DateTimeOffset.UnixEpoch);
|
|
var unknown = BrowserPlayPolicy.Evaluate(Candidate(gameId, "roms/psx/Game/Game.rar", 512),
|
|
true, true, DateTimeOffset.UnixEpoch);
|
|
|
|
Assert.True(qualified.Available);
|
|
// Rule 10: an implemented delivery path is not a support claim until a fixture proves it.
|
|
Assert.False(candidate.Available);
|
|
Assert.Equal(BrowserPlayState.UnsupportedFormat, candidate.State);
|
|
Assert.Contains("not release-qualified", candidate.Message, StringComparison.Ordinal);
|
|
Assert.False(unknown.Available);
|
|
Assert.Contains("not allowlisted", unknown.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void OnlyTheFixtureProvenPlayStation2ContainerIsClaimedPlayable()
|
|
{
|
|
var ps2 = BrowserPlayPolicy.GetPlatform("ps2")!;
|
|
|
|
// Play!.js range-reads a raw block device and boots it as an ISO. Only the ISO fixture has
|
|
// completed the live gate, so a compressed container is deliverable, never claimed.
|
|
Assert.True(ps2.IsQualified(".iso"));
|
|
foreach (var candidate in new[] { ".cso", ".chd", ".isz", ".bin" })
|
|
{
|
|
Assert.True(ps2.Accepts(candidate), candidate);
|
|
Assert.False(ps2.IsQualified(candidate), candidate);
|
|
Assert.False(BrowserPlayPolicy.Evaluate(
|
|
Candidate(Guid.NewGuid(), $"roms/ps2/Game/Game{candidate}", 4_000_000_000),
|
|
true, true, DateTimeOffset.UnixEpoch).Available, candidate);
|
|
}
|
|
Assert.True(BrowserPlayPolicy.Evaluate(Candidate(Guid.NewGuid(), "roms/ps2/Game/Game.iso", 4_000_000_000),
|
|
true, true, DateTimeOffset.UnixEpoch).Available);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("snes", ".sfc", ".smc")]
|
|
[InlineData("psx", ".chd", ".pbp")]
|
|
[InlineData("psp", ".pbp", ".iso")]
|
|
[InlineData("psp", ".pbp", ".cso")]
|
|
[InlineData("genesis", ".md", ".gen")]
|
|
[InlineData("genesis", ".md", ".smd")]
|
|
[InlineData("genesis", ".md", ".bin")]
|
|
public void ContainerClaimsStayBoundToTheExactLiveFixture(string platform, string qualifiedExtension,
|
|
string candidateExtension)
|
|
{
|
|
var mapping = BrowserPlayPolicy.GetPlatform(platform)!;
|
|
|
|
Assert.True(mapping.IsQualified(qualifiedExtension));
|
|
Assert.True(mapping.Accepts(candidateExtension));
|
|
Assert.False(mapping.IsQualified(candidateExtension));
|
|
var result = BrowserPlayPolicy.Evaluate(
|
|
Candidate(Guid.NewGuid(), $"roms/{platform}/Fixture/Game{candidateExtension}", 64 * 1024),
|
|
true, true, DateTimeOffset.UnixEpoch);
|
|
Assert.False(result.Available);
|
|
Assert.Equal(BrowserPlayState.UnsupportedFormat, result.State);
|
|
Assert.Contains("not release-qualified", result.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
private static BrowserPlayCandidate Candidate(Guid gameId, string path, long size) =>
|
|
new(gameId, Guid.NewGuid(), "Fixture", path.Split('/')[1], path, size, null, true);
|
|
|
|
}
|