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

157 lines
7.3 KiB
C#

using System.Text;
using Ludarium.Application;
using Ludarium.Domain;
using Ludarium.Infrastructure;
namespace Ludarium.IntegrationTests;
public sealed class GameDataVaultFileStoreTests
{
[Fact]
public async Task OpaqueRevisionIsHashedIdempotentDownloadableAndDeletable()
{
var directory = Path.Combine(Path.GetTempPath(), "ludarium-game-data-" + Guid.NewGuid().ToString("N"));
try
{
var store = new GameDataVaultFileStore(directory);
var gameId = Guid.NewGuid();
var entryId = Guid.NewGuid();
var revisionId = Guid.NewGuid();
var payload = Encoding.ASCII.GetBytes("SYNTHETIC-SAVE\0slot=1\0progress=42");
var writes = await Task.WhenAll(
store.SaveAsync(gameId, entryId, revisionId, "slot-1.sav", new MemoryStream(payload),
payload.Length, "integration-test", CancellationToken.None),
store.SaveAsync(gameId, entryId, revisionId, "slot-1.sav", new MemoryStream(payload),
payload.Length, "integration-test", CancellationToken.None));
var revision = writes[0];
var retried = writes[1];
Assert.Equal(revision.Sha256, retried.Sha256);
Assert.Equal(64, revision.Sha256.Length);
Assert.Single(Directory.EnumerateFiles(directory, "*.bin", SearchOption.AllDirectories));
var opened = Assert.IsType<StoredGameDataRevision>(await store.OpenAsync(revision, CancellationToken.None));
await using (opened.Content)
{
using var copy = new MemoryStream();
await opened.Content.CopyToAsync(copy);
Assert.Equal(payload, copy.ToArray());
}
var conflictingPayload = Enumerable.Repeat((byte)0x5A, payload.Length).ToArray();
await Assert.ThrowsAsync<ResourceConflictException>(() => store.SaveAsync(gameId, entryId,
revisionId, "slot-1.sav", new MemoryStream(conflictingPayload), conflictingPayload.Length,
"integration-test", CancellationToken.None));
Assert.True(store.Delete(revision));
Assert.Null(await store.OpenAsync(revision, CancellationToken.None));
}
finally
{
if (Directory.Exists(directory)) Directory.Delete(directory, true);
}
}
[Theory]
[InlineData("MZsynthetic")]
[InlineData("#!/bin/sh")]
public async Task ExecutableAndScriptSignaturesAreRejected(string value)
{
var directory = Path.Combine(Path.GetTempPath(), "ludarium-game-data-" + Guid.NewGuid().ToString("N"));
try
{
var store = new GameDataVaultFileStore(directory);
var payload = Encoding.ASCII.GetBytes(value);
await Assert.ThrowsAsync<GameDataVaultValidationException>(() => store.SaveAsync(Guid.NewGuid(),
Guid.NewGuid(), Guid.NewGuid(), "slot.sav", new MemoryStream(payload), payload.Length,
"integration-test", CancellationToken.None));
Assert.Empty(Directory.EnumerateFiles(directory, "*.bin", SearchOption.AllDirectories));
}
finally
{
if (Directory.Exists(directory)) Directory.Delete(directory, true);
}
}
[Fact]
public async Task SameLengthCorruptionFailsClosedOnOpen()
{
var directory = Path.Combine(Path.GetTempPath(), "ludarium-game-data-" + Guid.NewGuid().ToString("N"));
try
{
var store = new GameDataVaultFileStore(directory);
var payload = Encoding.ASCII.GetBytes("SYNTHETIC-SAVE-CONTENT");
var revision = await store.SaveAsync(Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), "slot.sav",
new MemoryStream(payload), payload.Length, "integration-test", CancellationToken.None);
var path = Path.Combine(directory, revision.Location.Replace('/', Path.DirectorySeparatorChar));
await File.WriteAllBytesAsync(path, Enumerable.Repeat((byte)0x41, payload.Length).ToArray());
await Assert.ThrowsAsync<GameDataIntegrityException>(() => store.OpenAsync(revision, CancellationToken.None));
}
finally
{
if (Directory.Exists(directory)) Directory.Delete(directory, true);
}
}
[Fact]
public async Task DeclaredOversizeAndNativeExecutableSignaturesAreRejected()
{
var directory = Path.Combine(Path.GetTempPath(), "ludarium-game-data-" + Guid.NewGuid().ToString("N"));
try
{
var store = new GameDataVaultFileStore(directory);
await Assert.ThrowsAsync<GameDataVaultValidationException>(() => store.SaveAsync(Guid.NewGuid(),
Guid.NewGuid(), Guid.NewGuid(), "slot.sav", new MemoryStream([0x01]),
GameDataVaultFileStore.MaximumBytes + 1, "integration-test", CancellationToken.None));
byte[][] signatures =
[
[0x7F, 0x45, 0x4C, 0x46, 0x00],
[0xFE, 0xED, 0xFA, 0xCF, 0x00],
[0xCA, 0xFE, 0xBA, 0xBE, 0x00]
];
foreach (var signature in signatures)
await Assert.ThrowsAsync<GameDataVaultValidationException>(() => store.SaveAsync(Guid.NewGuid(),
Guid.NewGuid(), Guid.NewGuid(), "slot.sav", new MemoryStream(signature), signature.Length,
"integration-test", CancellationToken.None));
Assert.Empty(Directory.EnumerateFiles(directory, "*.bin", SearchOption.AllDirectories));
}
finally
{
if (Directory.Exists(directory)) Directory.Delete(directory, true);
}
}
[Fact]
public async Task ReconciliationRemovesOnlyAppOwnedIncompleteAndOrphanRevisionFiles()
{
var directory = Path.Combine(Path.GetTempPath(), "ludarium-game-data-" + Guid.NewGuid().ToString("N"));
try
{
var store = new GameDataVaultFileStore(directory);
var payload = Encoding.ASCII.GetBytes("SYNTHETIC-SAVE-CONTENT");
var revision = await store.SaveAsync(Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), "slot.sav",
new MemoryStream(payload), payload.Length, "integration-test", CancellationToken.None);
var orphan = Path.Combine(directory, "orphan", "revision.bin");
var uploading = Path.Combine(directory, "orphan", "revision.uploading");
var unrelated = Path.Combine(directory, "orphan", "operator-note.txt");
Directory.CreateDirectory(Path.GetDirectoryName(orphan)!);
await File.WriteAllBytesAsync(orphan, [1, 2, 3]);
await File.WriteAllBytesAsync(uploading, [4, 5]);
await File.WriteAllTextAsync(unrelated, "retain");
var result = store.Reconcile(new HashSet<string>(StringComparer.OrdinalIgnoreCase) { revision.Location });
Assert.Equal(1, result.RemovedUploads);
Assert.Equal(1, result.RemovedOrphans);
Assert.Equal(5, result.ReclaimedBytes);
Assert.True(File.Exists(Path.Combine(directory, revision.Location.Replace('/', Path.DirectorySeparatorChar))));
Assert.True(File.Exists(unrelated));
Assert.False(File.Exists(orphan));
Assert.False(File.Exists(uploading));
}
finally
{
if (Directory.Exists(directory)) Directory.Delete(directory, true);
}
}
}