using System.Net; using System.Net.Http; using System.Reflection; using game.PlayerDataStorage; using Newtonsoft.Json; static partial class Tests { static async Task RunCP2AExistingArchiveTests() { await Run("CP2A-formal-load-rejects-new-or-empty-archive-before-apply", () => OnMain(CP2AExistingArchive)); await Run("CP2A-formal-transport-rejects-shared-inner-before-login", () => OnMain(CP2ASharedInner)); await Run("CP2A-formal-close-failure-revokes-handle-and-blocks-next", () => OnMain(CP2ACloseFailure)); await Run("CP2A-formal-sockets-without-cookie-contract-rejected", () => { var capability=PlayerKVControlledLoadCapability.CreateForGame(_ => new SocketsHttpHandler()); bool rejected=false; try { using var handler=capability.CreateHandler(new CookieContainer()); } catch(InvalidOperationException) { rejected=true; } Check(rejected,"formal factory must not accept an unverified SocketsHttpHandler cookie policy"); return Task.CompletedTask; }); } static async Task CP2ACloseFailure() { var f = new LoadFixture(); var formal = new Uri(PlayerKVRuntimeSelection.FormalApiUrl); var capability = PlayerKVGameLoadCapability.CreateForGame( new PlayerKVIdentitySnapshot(null, false, PlayerKVIdentityChannel.none, Array.Empty()), SchemaSnapshot(), formal, Path.Combine(Root, "cp2a", Guid.NewGuid().ToString("N")), jar => new Handler((_, _) => { jar.Add(formal, new Cookie("sid", "synthetic")); return Task.FromResult(Json(JsonConvert.SerializeObject(new { state = 0, internalId = 17, newCreate = false, createTime = "2026-01-01T00:00:00Z", svrTime = "2026-01-02T00:00:00Z" }))); }), _ => new Handler((_, _) => Task.FromResult(Json(JsonConvert.SerializeObject(new { data = ModernRaw() })))), _ => new Handler((_, _) => Task.FromResult(Json("{}")), () => throw new InvalidOperationException("synthetic close"))); var result = await f.Coordinator.LoadForGameAsync("101", capability); var hasOriginal=f.Coordinator.TryGetCurrentGameHandle(result.Epoch, out var original); Check(result.State == PlayerKVGameLoadState.GameLoaded && hasOriginal, "formal handle published"); var closing = f.Coordinator.Revoke(); Check(!f.Coordinator.TryGetCurrentGameHandle(result.Epoch, out _) && !original.ReadLocalSnapshot().Success, "close invalidates handle synchronously"); try { await closing; throw new Exception("close fault was ignored"); } catch(InvalidOperationException ex) { Check(ex.Message.Contains("synthetic close"), "close failure retained"); } var next = new LoadFixture(f.Registry); var denied = await next.Coordinator.LoadForGameAsync("102", next.Capability()); Check(denied.Error == PlayerKVGameLoadError.PreviousCloseFailed && next.Auth == 0, "failed close prevents next context and sends no authentication"); } sealed class CP2AWrapper : DelegatingHandler { internal CP2AWrapper(HttpMessageHandler inner) { InnerHandler = inner; } } static async Task CP2ASharedInner() { var f = new LoadFixture(); int sent = 0; var shared = new Handler((_, _) => { sent++; return Task.FromResult(Json("{}")); }); var capability = PlayerKVGameLoadCapability.CreateForGame( new PlayerKVIdentitySnapshot(null, false, PlayerKVIdentityChannel.none, Array.Empty()), SchemaSnapshot(), new Uri(PlayerKVRuntimeSelection.FormalApiUrl), Path.Combine(Root, "cp2a", Guid.NewGuid().ToString("N")), _ => new CP2AWrapper(shared), _ => new CP2AWrapper(shared), _ => new Handler((_, _) => Task.FromResult(Json("{}")))); var result = await f.Coordinator.LoadForGameAsync("101", capability); Check(result.State == PlayerKVGameLoadState.Rejected && sent == 0, "shared inner handler graph must fail before account/Login"); await f.Coordinator.Revoke(); } static async Task CP2AExistingArchive() { var method = typeof(PlayerKVGameLoadCapability).GetMethod("CreateForGame", BindingFlags.Static | BindingFlags.NonPublic); Check(method != null, "formal factory must exist without using editor validation factory"); var formal = new Uri("http://s101.local:18080/"); foreach (var (newlyCreated, empty, accepted) in new[] { (true, false, false), (false, true, false), (false, false, true) }) { var f = new LoadFixture(); CookieContainer jar = null; Func auth = cookies => { jar = cookies; return new Handler((request, _) => { Check(request.RequestUri.AbsolutePath == "/account/Login", "exactly one account login route"); f.Auth++; cookies.Add(formal, new Cookie("sid", "synthetic")); return Task.FromResult(Json(JsonConvert.SerializeObject(new { state = 0, internalId = 17, newCreate = newlyCreated, createTime = "2026-01-01T00:00:00Z", svrTime = "2026-01-02T00:00:00Z" }))); }); }; Func kv = cookies => { Check(ReferenceEquals(cookies, jar), "KV uses authentication cookie jar"); return new Handler((request, _) => { Check(request.RequestUri.AbsolutePath == "/playerdata/Get", "read-only KV fetch"); f.Gets++; return Task.FromResult(Json(JsonConvert.SerializeObject(new { data = empty ? new Dictionary() : ModernRaw() }))); }); }; Func lease = cookies => { Check(ReferenceEquals(cookies, jar), "account lease uses authentication cookie jar"); return new Handler((_, _) => throw new InvalidOperationException("CP2A must send no business API request")); }; var capability = (PlayerKVGameLoadCapability)method.Invoke(null, new object[] { new PlayerKVIdentitySnapshot(null, false, PlayerKVIdentityChannel.none, Array.Empty()), SchemaSnapshot(), formal, Path.Combine(Root, "cp2a", Guid.NewGuid().ToString("N")), auth, kv, lease }); try { var result = await f.Coordinator.LoadForGameAsync("101", capability); Check((result.State == PlayerKVGameLoadState.GameLoaded) == accepted, "only a pre-existing complete archive becomes game-loaded"); Check(f.Auth == 1 && f.Gets == 1, "no second login or KV fetch"); Check(f.Notify == (accepted ? 1 : 0), "ineligible archive never reaches Apply notification"); if (accepted) Check(f.Coordinator.TryGetCurrentGameHandle(result.Epoch, out var owner) && owner.PlayerId == "101", "eligible archive yields original fixed account handle"); else Check(!f.Coordinator.TryGetCurrentGameHandle(result.Epoch, out _), "rejected archive has no usable handle"); } finally { await f.Coordinator.Revoke(); } } } }