using System; using System.Linq; using System.Reflection; using FluentAssertions; using GameCult.Caching; using GameCult.Caching.MessagePack; using GameCult.Networking; using NUnit.Framework; namespace GameCult.Mesh.Tests; // The mesh's typed reads of a raw record decode the payload through the one-value rule: a tail is refused, never ignored. public class BytesAfterPathTests { private const string Clause = "bytes after its first MessagePack value"; private static CultNetRawDocumentRecord WithTail(CultMeshCdnArtifactManifest manifest) { var payload = CultDocumentMessagePackSerialization.SerializeUntyped(manifest, typeof(CultMeshCdnArtifactManifest)).Concat(new byte[] { 0xc0 }).ToArray(); return new CultNetRawDocumentRecord { SchemaId = CultDocumentRegistry.Shared.GetRequired().SchemaId, RecordKey = "k", StoredAt = "1970-01-01T00:00:00.000Z", Payload = payload, }; } private static CultMeshCdnArtifactManifest Manifest() => new() { ArtifactId = "a", Version = "1", ContentHash = "h" }; private static string Refusal(Action read) { var thrown = Assert.Catch(read)!; return thrown is TargetInvocationException { InnerException: { } inner } ? inner.ToString() : thrown.ToString(); } [Test] public void ASnapshotDecodeThroughABindingRefusesATail() { var registry = CultMeshCdn.CreateDocumentRegistry(); var snapshot = new CultNetSnapshotResponseRawMessage { Documents = new[] { WithTail(Manifest()) } }; Refusal(() => CultMesh.DecodeSnapshotDocuments(snapshot, registry)).Should().Contain(Clause); } [Test] public void ASnapshotDecodeWithNoBindingRefusesATail() { var snapshot = new CultNetSnapshotResponseRawMessage { Documents = new[] { WithTail(Manifest()) } }; Refusal(() => CultMesh.DecodeSnapshotDocuments(snapshot, new CultNetDocumentRegistry())).Should().Contain(Clause); } [Test] public void ATypedReadOfOneSnapshotRecordRefusesATail() { var record = WithTail(Manifest()); var snapshot = new CultNetSnapshotResponseRawMessage { MessageId = "m", Documents = new[] { record } }; var read = typeof(CultMesh).GetMethod("ReadDocumentFromSnapshotResponse", BindingFlags.NonPublic | BindingFlags.Static)! .MakeGenericMethod(typeof(CultMeshCdnArtifactManifest)); Refusal(() => read.Invoke(null, new object[] { snapshot, record.SchemaId, record.RecordKey })).Should().Contain(Clause); } [Test] public void ASingleFileDocumentReadRefusesATail() { var path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"cultlib-bytesafter-mesh-{Guid.NewGuid():N}.cc"); try { var descriptor = CultDocumentRegistry.Shared.GetRequired(); var schema = new CultMeshSingleFileDocumentSchema(descriptor.SchemaId, descriptor.SchemaName, descriptor.SchemaVersion); var payload = WithTail(Manifest()).Payload; CultMesh.WriteSingleFileDocumentPayload(path, new CultRecordKey("k"), schema, null, payload); Refusal(() => CultMesh.ReadSingleFileDocument(path, new CultRecordKey("k"))).Should().Contain(Clause); } finally { System.IO.File.Delete(path); } } }