382 lines
26 KiB
C#
382 lines
26 KiB
C#
using Ludarium.Application;
|
|
using Ludarium.Domain;
|
|
|
|
namespace Ludarium.ArchitectureTests;
|
|
|
|
public sealed class DependencyTests
|
|
{
|
|
[Fact]
|
|
public void DomainHasNoInfrastructureDependencies()
|
|
{
|
|
var references = typeof(LibraryRoot).Assembly.GetReferencedAssemblies().Select(x => x.Name).ToArray();
|
|
Assert.DoesNotContain(references, x => x is not null && (x.StartsWith("Npgsql", StringComparison.Ordinal) || x.StartsWith("Microsoft.AspNetCore", StringComparison.Ordinal)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplicationDoesNotReferenceInfrastructure()
|
|
{
|
|
var references = typeof(IScanCoordinator).Assembly.GetReferencedAssemblies().Select(x => x.Name).ToArray();
|
|
Assert.DoesNotContain("Ludarium.Infrastructure", references);
|
|
}
|
|
|
|
[Fact]
|
|
public void ContainerSupervisorAndHealthProbeFailClosedOnDatabaseLoss()
|
|
{
|
|
var root = FindRepositoryRoot();
|
|
var entrypoint = File.ReadAllText(Path.Combine(root, "deploy", "ludarium-entrypoint.sh"));
|
|
var healthProbe = File.ReadAllText(Path.Combine(root, "src", "Ludarium.HealthCheck", "Program.cs"));
|
|
|
|
Assert.Contains("wait -n \"$postgres_pid\" \"$app_pid\"", entrypoint, StringComparison.Ordinal);
|
|
Assert.Contains("PostgreSQL exited unexpectedly", entrypoint, StringComparison.Ordinal);
|
|
Assert.Contains("terminate_children", entrypoint, StringComparison.Ordinal);
|
|
Assert.Contains("/health/ready", healthProbe, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("/health/live", healthProbe, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnraidImageUsesPatchedUnifiedNobleRuntimeBaseline()
|
|
{
|
|
var root = FindRepositoryRoot();
|
|
var dockerfile = File.ReadAllText(Path.Combine(root, "Dockerfile.unraid"));
|
|
var entrypoint = File.ReadAllText(Path.Combine(root, "deploy", "ludarium-entrypoint.sh"));
|
|
var collationRepair = File.ReadAllText(Path.Combine(root, "deploy", "repair-postgres-collation.sh"));
|
|
|
|
Assert.Contains("FROM postgres:16.15-bookworm@sha256:60f4761b9035e0b8d5218f701a8c3382f641bf12b1604822574cf5be3baeb537 AS postgres-entrypoint", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("FROM mcr.microsoft.com/dotnet/aspnet:10.0.11-noble@sha256:a4556ed033fa96f984bb7a8d348851cb2d36b1281dd2420070045f664fbb5f94 AS final", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("ARG DEBIAN_FRONTEND=noninteractive", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("LANG=C.UTF-8", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("LC_ALL=C.UTF-8", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("POSTGRES_PACKAGE_VERSION=16.15-0ubuntu0.24.04.1", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("GOSU_PACKAGE_VERSION=1.17-1ubuntu0.24.04.3", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("LOCALES_PACKAGE_VERSION=2.39-0ubuntu8.8", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("locale-gen en_US.UTF-8", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("locale -a | grep -Fx 'en_US.utf8'", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("COPY --from=postgres-entrypoint /usr/local/bin/docker-entrypoint.sh", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("COPY --chmod=755 deploy/repair-postgres-collation.sh", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("mkdir -p \"$PGDATA\" /docker-entrypoint-initdb.d", dockerfile, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("postgres:16.14-alpine", dockerfile, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("FROM postgres:16.15-bookworm AS final", dockerfile, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("apk add", dockerfile[LastStageStart(dockerfile)..], StringComparison.Ordinal);
|
|
Assert.Contains("gosu \"${PUID:-1654}:${PGID:-1654}\"", entrypoint, StringComparison.Ordinal);
|
|
Assert.Contains("/usr/local/bin/repair-postgres-collation.sh", entrypoint, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("su-exec", entrypoint, StringComparison.Ordinal);
|
|
Assert.Contains("REINDEX DATABASE", collationRepair, StringComparison.Ordinal);
|
|
Assert.Contains("REFRESH COLLATION VERSION", collationRepair, StringComparison.Ordinal);
|
|
Assert.Contains("pg_database_collation_actual_version(oid)", collationRepair, StringComparison.Ordinal);
|
|
Assert.Contains("datcollversion is null", collationRepair, StringComparison.Ordinal);
|
|
Assert.Contains("semantic_variants", collationRepair, StringComparison.Ordinal);
|
|
Assert.Contains("contains_custom", collationRepair, StringComparison.Ordinal);
|
|
Assert.Contains("refusing automatic data changes", collationRepair, StringComparison.Ordinal);
|
|
Assert.Contains("^[A-Za-z_][A-Za-z0-9_]*$", collationRepair, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProductionImagesSharePinnedPlayJsProvenance()
|
|
{
|
|
var root = FindRepositoryRoot();
|
|
var dockerfile = PlayJsStage(File.ReadAllText(Path.Combine(root, "Dockerfile")));
|
|
var unraidDockerfile = PlayJsStage(File.ReadAllText(Path.Combine(root, "Dockerfile.unraid")));
|
|
|
|
Assert.Equal(dockerfile, unraidDockerfile);
|
|
Assert.Contains("PLAYJS_UPSTREAM_COMMIT=04bde0df87ee7c0e2f0151b51bb2cc22c88541da", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("PLAYJS_WASM_SHA256=de3ae0efdd7938d1b5ac754a096ec180094d64702ae84b962c4bb9f79abb1da7", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("raw.githubusercontent.com/jpd002/Play-/${PLAYJS_UPSTREAM_COMMIT}/License.txt", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("/playjs/PROVENANCE.txt", dockerfile, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProductionImagesShareOfflineEmulatorJsPatch()
|
|
{
|
|
var root = FindRepositoryRoot();
|
|
var standardImage = File.ReadAllText(Path.Combine(root, "Dockerfile"));
|
|
var unraidImage = File.ReadAllText(Path.Combine(root, "Dockerfile.unraid"));
|
|
var dockerfile = EmulatorJsStage(standardImage);
|
|
var unraidDockerfile = EmulatorJsStage(unraidImage);
|
|
var managedGate = File.ReadAllText(Path.Combine(root, "deploy", "run-managed-validation.sh"));
|
|
var mainCompose = File.ReadAllText(Path.Combine(root, "deploy", "compose.yml"));
|
|
var managedWorkflowPath = Path.Combine(root, ".gitea", "workflows", "managed-validation.yml");
|
|
var deployWorkflowPath = Path.Combine(root, ".gitea", "workflows", "unraid-deploy.yml");
|
|
var publicWorkflowPath = Path.Combine(root, ".gitea", "workflows", "public-validation.yml");
|
|
var managedWorkflow = File.Exists(managedWorkflowPath) ? File.ReadAllText(managedWorkflowPath) : "";
|
|
var deployWorkflow = File.Exists(deployWorkflowPath) ? File.ReadAllText(deployWorkflowPath) : "";
|
|
var publicWorkflow = File.Exists(publicWorkflowPath) ? File.ReadAllText(publicWorkflowPath) : "";
|
|
|
|
Assert.Equal(dockerfile, unraidDockerfile);
|
|
Assert.Contains("EMULATORJS_VERSION=4.2.3", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("COPY .build-inputs/emulatorjs/ /tmp/emulatorjs-input/", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("s#https://cdn.emulatorjs.org/stable/data/version.json#/emulatorjs/data/version.json#g", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("/emulatorjs/LUDARIUM_PATCHES.txt", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("--dns 1.1.1.1 --dns 8.8.8.8", managedGate, StringComparison.Ordinal);
|
|
Assert.Contains("07d451bc06fa3ad04ab30d9b94eb63ac34ad0babee52d60357b002bde8f3850b", managedGate, StringComparison.Ordinal);
|
|
Assert.Contains("cleanup_build_inputs", managedGate, StringComparison.Ordinal);
|
|
foreach (var workflow in new[] { managedWorkflow, deployWorkflow, publicWorkflow }.Where(x => x.Length > 0))
|
|
{
|
|
Assert.Contains("actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020", workflow, StringComparison.Ordinal);
|
|
}
|
|
if (managedWorkflow.Length > 0)
|
|
{
|
|
Assert.Contains("run-managed-validation.sh source", managedWorkflow, StringComparison.Ordinal);
|
|
var manualJobStart = managedWorkflow.IndexOf("\n manual:\n", StringComparison.Ordinal);
|
|
Assert.True(manualJobStart >= 0);
|
|
Assert.DoesNotContain("DOCKER_HOST:", managedWorkflow[..manualJobStart], StringComparison.Ordinal);
|
|
}
|
|
if (deployWorkflow.Length > 0)
|
|
{
|
|
Assert.Contains("DOCKER_HOST: tcp://172.17.0.1:2375", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.Contains("LUDARIUM_DOCKER_BUILD_NETWORK: host", deployWorkflow, StringComparison.Ordinal);
|
|
}
|
|
if (publicWorkflow.Length > 0)
|
|
{
|
|
Assert.Contains("run-managed-validation.sh source", publicWorkflow, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("DOCKER_HOST:", publicWorkflow, StringComparison.Ordinal);
|
|
}
|
|
foreach (var image in new[] { standardImage, unraidImage })
|
|
{
|
|
Assert.Contains("org.opencontainers.image.revision", image, StringComparison.Ordinal);
|
|
Assert.Contains("org.opencontainers.image.version", image, StringComparison.Ordinal);
|
|
}
|
|
Assert.Contains("LUDARIUM_SOURCE_REVISION: ${GITEA_COMMIT_SHA:-local}", mainCompose, StringComparison.Ordinal);
|
|
if (deployWorkflow.Length > 0)
|
|
{
|
|
var deployJobStart = deployWorkflow.IndexOf("\n deploy:\n", StringComparison.Ordinal);
|
|
Assert.True(deployJobStart >= 0);
|
|
var deployJob = deployWorkflow[deployJobStart..];
|
|
Assert.DoesNotContain("uses:", deployJob, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("< VERSION", deployJob, StringComparison.Ordinal);
|
|
var runnerContract = deployJob.IndexOf("Validate deploy runner contract and exact revision", StringComparison.Ordinal);
|
|
var deployBackup = deployJob.IndexOf("Create pre-promotion database backup", StringComparison.Ordinal);
|
|
var deployPromote = deployJob.IndexOf("Deploy exact Gitea revision", StringComparison.Ordinal);
|
|
Assert.True(runnerContract >= 0 && runnerContract < deployBackup && deployBackup < deployPromote);
|
|
Assert.Contains("^[0-9a-f]{40}$", deployJob, StringComparison.Ordinal);
|
|
Assert.Contains("promotion refused: GITHUB_SHA is not a 40-character", deployJob, StringComparison.Ordinal);
|
|
Assert.Contains("promotion refused: unexpected repository", deployJob, StringComparison.Ordinal);
|
|
Assert.Contains("command -v \"$tool\"", deployJob, StringComparison.Ordinal);
|
|
Assert.Contains("promotion refused: the gitea-deploy-control controller is not running", deployJob, StringComparison.Ordinal);
|
|
Assert.Contains("Create pre-promotion database backup", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.Contains("docker exec Ludarium pg_dump -U ludarium -Fc ludarium", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.Contains("docker exec -i Ludarium pg_restore --list", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.Contains("/mnt/user/appdata/ludarium/backups/pre-${GITHUB_SHA}", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("source/deploy/data/backups", deployJob, StringComparison.Ordinal);
|
|
Assert.Contains("partial_path=\"${backup_path}.partial\"", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.Contains("promotion refused: the production database is not running", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.Contains("Verify exact live image identity and source policy", deployWorkflow, StringComparison.Ordinal);
|
|
var revisionGate = deployJob.IndexOf("prepared_revision\" != \"$GITHUB_SHA", StringComparison.Ordinal);
|
|
var versionRead = deployJob.IndexOf("/mnt/user/appdata/ludarium/source/VERSION", StringComparison.Ordinal);
|
|
Assert.True(revisionGate >= 0 && versionRead > revisionGate);
|
|
Assert.Contains("promotion refused: the synced VERSION is not a release identifier", deployJob, StringComparison.Ordinal);
|
|
Assert.Contains("promotion refused: the pre-promotion backup did not survive", deployJob, StringComparison.Ordinal);
|
|
Assert.Contains("test \"$actual_image\" = \"$expected_image\"", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.Contains("test \"$actual_revision\" = \"$GITHUB_SHA\"", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.Contains("test \"$actual_version\" = \"$release\"", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.Contains("restart-baseline", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.Contains("if [ \"$actual_id\" = \"$baseline_id\" ]", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.Contains("if [ \"$restart_count\" != \"$expected_restarts\" ]", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.Contains("test \"$readonly_root\" = \"true\"", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.Contains("test \"$no_new_privileges\" = '[\"no-new-privileges:true\"]'", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.Contains("test \"$cap_drop\" = '[\"ALL\"]'", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.Contains("test \"$cap_add\" = '[\"CAP_CHOWN\",\"CAP_DAC_OVERRIDE\",\"CAP_FOWNER\",\"CAP_KILL\",\"CAP_SETGID\",\"CAP_SETUID\"]'", deployWorkflow, StringComparison.Ordinal);
|
|
Assert.Contains("for dest in /library/games /library/ps4 /library/ps5", deployWorkflow, StringComparison.Ordinal);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SwitchControllerDiscoversTheActivePixelFluxDisplay()
|
|
{
|
|
var root = FindRepositoryRoot();
|
|
var shared = File.ReadAllText(Path.Combine(root, "deploy", "ludarium_sidecar.py"));
|
|
var compose = File.ReadAllText(Path.Combine(root, "deploy", "compose.switch.yml"));
|
|
|
|
// The active display is read from the running emulator process; the socket scan is only a
|
|
// bounded fallback. This lived in the Eden controller alone, which is exactly how the
|
|
// Dolphin sidecar shipped without it.
|
|
Assert.Contains("def desktop_display(process_name):", shared, StringComparison.Ordinal);
|
|
Assert.Contains("/proc/{pid}/environ", shared, StringComparison.Ordinal);
|
|
Assert.Contains("Path(\"/tmp/.X11-unix\").glob(\"X*\")", shared, StringComparison.Ordinal);
|
|
Assert.Contains("\"DISPLAY\": desktop_display(process_name)", shared, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("\"DISPLAY\": \":0\"", shared, StringComparison.Ordinal);
|
|
|
|
// Every sidecar must obtain its runtime environment from the shared module, never its own copy.
|
|
foreach (var controller in new[] { "eden-controller.py", "dolphin-controller.py" })
|
|
{
|
|
var text = File.ReadAllText(Path.Combine(root, "deploy", controller));
|
|
Assert.Contains("import ludarium_sidecar as sidecar", text, StringComparison.Ordinal);
|
|
Assert.Contains("sidecar.EmulatorRuntime(", text, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("def desktop_display", text, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("subprocess.Popen", text, StringComparison.Ordinal);
|
|
}
|
|
foreach (var dockerfile in new[] { "Dockerfile.eden-controller", "Dockerfile.dolphin-controller" })
|
|
Assert.Contains("deploy/ludarium_sidecar.py /opt/ludarium/ludarium_sidecar.py",
|
|
File.ReadAllText(Path.Combine(root, dockerfile)), StringComparison.Ordinal);
|
|
|
|
Assert.Contains("lscr.io/linuxserver/eden@sha256:f18ec24d55266daab9a440aba5fe3e9587494cf7a9aad63f93806efe4081be83",
|
|
File.ReadAllText(Path.Combine(root, "Dockerfile.eden-controller")), StringComparison.Ordinal);
|
|
Assert.Contains("ludarium/eden-controller:0.4.9-rc.1", compose, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void SwitchSidecarRemovesUnusedPrivilegedToolingAndRequiresHealth()
|
|
{
|
|
var root = FindRepositoryRoot();
|
|
var dockerfile = File.ReadAllText(Path.Combine(root, "Dockerfile.eden-controller"));
|
|
var compose = File.ReadAllText(Path.Combine(root, "deploy", "compose.switch.yml"));
|
|
|
|
foreach (var package in new[]
|
|
{
|
|
"containerd.io", "docker-ce", "docker-ce-cli", "docker-compose-plugin",
|
|
"gcc", "g++", "make", "cmake", "git", "openssh-client", "cron", "curl", "perl"
|
|
})
|
|
Assert.Contains(package, dockerfile, StringComparison.Ordinal);
|
|
|
|
Assert.Contains("apt-get purge -y --auto-remove", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("contents.d/svc-cron", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("contents.d/svc-docker", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("condition: service_healthy", compose, StringComparison.Ordinal);
|
|
Assert.Contains("http://127.0.0.1:8765/health", compose, StringComparison.Ordinal);
|
|
Assert.Contains("no-new-privileges:true", compose, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("/var/run/docker.sock", compose, StringComparison.Ordinal);
|
|
|
|
var audit = File.ReadAllText(Path.Combine(root, "deploy", "audit-eden-runtime.sh"));
|
|
Assert.Contains("/games is not mounted read-only", audit, StringComparison.Ordinal);
|
|
Assert.Contains("nginx map directives are forbidden", audit, StringComparison.Ordinal);
|
|
Assert.Contains("Xwayland TCP listening is enabled", audit, StringComparison.Ordinal);
|
|
Assert.Contains("a runtime .netrc file is present", audit, StringComparison.Ordinal);
|
|
Assert.Contains("a RIST media URL is present", audit, StringComparison.Ordinal);
|
|
Assert.Contains("deploy/audit-eden-runtime.sh", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains("eden-controller-0.4.9.openvex.json", dockerfile, StringComparison.Ordinal);
|
|
|
|
var securityGate = File.ReadAllText(Path.Combine(root, "deploy", "run-eden-security-gate.sh"));
|
|
Assert.Contains("grype-0.116.1", securityGate, StringComparison.Ordinal);
|
|
Assert.Contains("--fail-on critical", securityGate, StringComparison.Ordinal);
|
|
Assert.Contains("VEX does not account for every raw Critical match", securityGate, StringComparison.Ordinal);
|
|
Assert.Contains("cmp -s \"$work/raw-ids\" \"$work/vex-ids\"", securityGate, StringComparison.Ordinal);
|
|
Assert.Contains("/games false", securityGate, StringComparison.Ordinal);
|
|
Assert.Contains("audit-eden-runtime.sh", securityGate, StringComparison.Ordinal);
|
|
|
|
var attributes = File.ReadAllText(Path.Combine(root, ".gitattributes"));
|
|
Assert.Contains("*.sh text eol=lf", attributes, StringComparison.Ordinal);
|
|
Assert.Contains("Dockerfile.* text eol=lf", attributes, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void DolphinSidecarIsPinnedExactTitleControlledAndReadOnly()
|
|
{
|
|
var root = FindRepositoryRoot();
|
|
var dockerfile = File.ReadAllText(Path.Combine(root, "Dockerfile.dolphin-controller"));
|
|
var compose = File.ReadAllText(Path.Combine(root, "deploy", "compose.dolphin.yml"));
|
|
var controller = File.ReadAllText(Path.Combine(root, "deploy", "dolphin-controller.py"));
|
|
|
|
Assert.Contains("dolphin:2606-ls78@sha256:7b916f1c97d591b96525b67128c1bf82ce6ede0eff6816fe1c0a3d70f744506d", dockerfile, StringComparison.Ordinal);
|
|
Assert.Contains(":/games/gamecube:ro", compose, StringComparison.Ordinal);
|
|
Assert.Contains(":/games/wii:ro", compose, StringComparison.Ordinal);
|
|
Assert.Contains("no-new-privileges:true", compose, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("/var/run/docker.sock", compose, StringComparison.Ordinal);
|
|
Assert.Contains("SELKIES_COMMAND_ENABLED: \"false|locked\"", compose, StringComparison.Ordinal);
|
|
Assert.Contains("LUDARIUM_DOLPHIN_FIXTURE_SHA256", compose, StringComparison.Ordinal);
|
|
Assert.Contains("extensions={\".iso\", \".gcm\", \".rvz\", \".gcz\", \".wbfs\", \".wia\"}", controller, StringComparison.Ordinal);
|
|
Assert.Contains("platforms={\"gamecube\", \"wii\"}", controller, StringComparison.Ordinal);
|
|
Assert.Contains("/usr/bin/dolphin-emu", controller, StringComparison.Ordinal);
|
|
Assert.Contains("save-state", controller, StringComparison.Ordinal);
|
|
Assert.Contains("load-state", controller, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnraidContainersExposeStableDockerManMetadata()
|
|
{
|
|
var root = FindRepositoryRoot();
|
|
var mainCompose = File.ReadAllText(Path.Combine(root, "deploy", "compose.yml"));
|
|
var switchCompose = File.ReadAllText(Path.Combine(root, "deploy", "compose.switch.yml"));
|
|
var dolphinCompose = File.ReadAllText(Path.Combine(root, "deploy", "compose.dolphin.yml"));
|
|
var template = File.ReadAllText(Path.Combine(root, "deploy", "unraid", "my-Ludarium.xml"));
|
|
|
|
Assert.Contains("net.unraid.docker.webui: \"${LUDARIUM_PUBLIC_URL:-http://[IP]:[PORT:1230]/}\"", mainCompose, StringComparison.Ordinal);
|
|
Assert.Contains("net.unraid.docker.webui: \"${SWITCH_PUBLIC_URL:-http://[IP]:[PORT:1262]/}\"", switchCompose, StringComparison.Ordinal);
|
|
Assert.Contains("net.unraid.docker.webui: \"${DOLPHIN_PUBLIC_URL:-http://[IP]:[PORT:1263]/}\"", dolphinCompose, StringComparison.Ordinal);
|
|
Assert.Contains("net.unraid.docker.managed: \"dockerman\"", mainCompose, StringComparison.Ordinal);
|
|
Assert.Contains("net.unraid.docker.managed: \"dockerman\"", switchCompose, StringComparison.Ordinal);
|
|
Assert.Contains("net.unraid.docker.managed: \"dockerman\"", dolphinCompose, StringComparison.Ordinal);
|
|
|
|
foreach (var document in new[] { mainCompose, switchCompose, dolphinCompose, template })
|
|
{
|
|
Assert.Contains("/boot/config/plugins/dockerMan/images/Ludarium-icon.svg", document, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("Ludarium-icon.png", document, StringComparison.Ordinal);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ReleaseEvidenceReadsTheSingleDeclaredSchemaVersion()
|
|
{
|
|
var root = FindRepositoryRoot();
|
|
var gate = File.ReadAllText(Path.Combine(root, "deploy", "run-candidate-gate.sh"));
|
|
var support = File.ReadAllText(Path.Combine(root, "src", "Ludarium.Infrastructure", "SupportBundleService.cs"));
|
|
|
|
Assert.Contains("CurrentSchemaVersion", gate, StringComparison.Ordinal);
|
|
Assert.Contains("candidate-schema${expected_schema}.dump", gate, StringComparison.Ordinal);
|
|
Assert.Contains("restored_schema\" = \"$expected_schema", gate, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("candidate-schema23", gate, StringComparison.Ordinal);
|
|
Assert.Contains("PostgresStore.CurrentSchemaVersion", support, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("schemaVersion = 23", support, StringComparison.Ordinal);
|
|
}
|
|
|
|
private static string PlayJsStage(string dockerfile)
|
|
{
|
|
const string startMarker = " AS playjs";
|
|
const string endMarker = " AS n64wasm";
|
|
var start = dockerfile.IndexOf(startMarker, StringComparison.Ordinal);
|
|
var end = dockerfile.IndexOf(endMarker, start, StringComparison.Ordinal);
|
|
Assert.True(start >= 0 && end > start, "The Play!.js image stage must be present.");
|
|
return dockerfile[start..end].ReplaceLineEndings("\n");
|
|
}
|
|
|
|
private static string EmulatorJsStage(string dockerfile)
|
|
{
|
|
const string startMarker = " AS emulator";
|
|
const string endMarker = " AS playjs";
|
|
var start = dockerfile.IndexOf(startMarker, StringComparison.Ordinal);
|
|
var end = dockerfile.IndexOf(endMarker, start, StringComparison.Ordinal);
|
|
Assert.True(start >= 0 && end > start, "The EmulatorJS image stage must be present.");
|
|
return dockerfile[start..end].ReplaceLineEndings("\n");
|
|
}
|
|
|
|
private static int LastStageStart(string dockerfile)
|
|
{
|
|
var start = dockerfile.LastIndexOf(" AS final", StringComparison.Ordinal);
|
|
Assert.True(start >= 0, "The patched unified final stage must be present.");
|
|
return start;
|
|
}
|
|
|
|
[Fact]
|
|
public void EveryMappedBrowserCoreStaysInTheImage()
|
|
{
|
|
var dockerfile = File.ReadAllText(Path.Combine(FindRepositoryRoot(), "Dockerfile"));
|
|
var retention = dockerfile.Split('\n')
|
|
.Single(line => line.Contains("/emulatorjs/data/cores", StringComparison.Ordinal) &&
|
|
line.Contains("-delete", StringComparison.Ordinal));
|
|
|
|
// The image build deletes every core it does not name. A platform mapped to a core that was
|
|
// not kept would fail at launch with no build-time signal.
|
|
foreach (var core in BrowserPlayPolicy.RequiredCores)
|
|
Assert.Contains($"! -name '{core}-*'", retention, StringComparison.Ordinal);
|
|
|
|
// N64 runs on Ludarium's own N64Wasm player, so no EmulatorJS N64 core can ever be selected.
|
|
Assert.DoesNotContain("mupen64plus_next", retention, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void EveryNativePlayerPlatformIsOutsideTheBrowserMatrix()
|
|
{
|
|
// A platform served by an isolated sidecar must never also claim a browser core: the game
|
|
// detail page picks one player, and two owners would make that choice arbitrary.
|
|
foreach (var platform in NativeRemotePlayerRegistry.Platforms)
|
|
Assert.Null(BrowserPlayPolicy.GetPlatform(platform));
|
|
}
|
|
|
|
private static string FindRepositoryRoot()
|
|
{
|
|
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Ludarium.slnx")))
|
|
directory = directory.Parent;
|
|
return directory?.FullName ?? throw new DirectoryNotFoundException("The Ludarium repository root was not found.");
|
|
}
|
|
}
|