157 lines
7.3 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|