using game.Account; using game.PlayerDataStorage; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Net; using System.Text; internal static class Import101Tests { static int assertions; static readonly List assertionFailures = new(); static void Check(bool condition, string message) { assertions++; if (!condition) { assertionFailures.Add(message); throw new InvalidOperationException("ASSERT: " + message); } } static JObject Window() => new JObject { ["playerId"] = "101", ["api"] = Import101.Api, ["windowId"] = "offline-fixture", ["operator"] = "Import101Tests", ["testOnly"] = true, ["startUtc"] = DateTime.UtcNow.AddMinutes(-1).ToString("o"), ["expiresUtc"] = DateTime.UtcNow.AddHours(1).ToString("o"), ["verificationMethods"] = new JArray("mock only; no real account window"), ["noOtherClientsDeclared"] = true, ["implementationReviewApproved"] = true }; sealed class Mock { internal string Fault; internal int Login, Get, Set; internal JObject Window; internal readonly Dictionary Expected; internal Dictionary Stored = new(StringComparer.Ordinal); internal string[] ReturnedOrder; internal Mock(Dictionary expected) { Expected = expected; } internal HttpMessageHandler Handler(CookieContainer jar) => new HandlerImpl(this, jar); sealed class HandlerImpl : HttpMessageHandler { readonly Mock mock; readonly CookieContainer jar; internal HandlerImpl(Mock mock, CookieContainer jar) { this.mock = mock; this.jar = jar; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken token) { Check(request.Method == HttpMethod.Post, "only POST"); Check(request.RequestUri.AbsoluteUri.StartsWith(Import101.Api, StringComparison.Ordinal), "fixed API"); var body = JObject.Parse(await request.Content.ReadAsStringAsync(token)); HttpResponseMessage Reply(object value, HttpStatusCode status = HttpStatusCode.OK) => new(status) { Content = new StringContent(JsonConvert.SerializeObject(value), Encoding.UTF8, "application/json") }; switch (request.RequestUri.AbsolutePath) { case "/account/Login": mock.Login++; Check(mock.Login == 1 && mock.Get == 0 && mock.Set == 0, "one initial login"); Check((string)body["playerId"] == "101" && body.Count == 1, "only 101"); jar.Add(new Uri(Import101.Api), new Cookie("session", "offline", "/")); if (mock.Fault == "login-http") return Reply(new { }, HttpStatusCode.Forbidden); if (mock.Fault == "login-protocol") return Reply(new { }); return Reply(new { state = 0, internalId = 101, newCreate = mock.Fault == "new-account", createTime = "2026-09-24T00:00:00Z", svrTime = "2026-09-28T07:30:00Z" }); case "/playerdata/Get": mock.Get++; Check(mock.Login == 1 && mock.Get <= 2 && (mock.Get == 1 ? mock.Set == 0 : mock.Set == 1), "Get order/bound"); Check(body.Count == 0, "full Get"); if (mock.Get == 1) { if (mock.Fault == "before-http") return Reply(new { }, HttpStatusCode.ServiceUnavailable); if (mock.Fault == "nonempty") return Reply(new { data = new { existing = "untouched" } }); return Reply(new { data = mock.Stored }); } if (mock.Fault == "after-http") return Reply(new { }, HttpStatusCode.ServiceUnavailable); if (mock.Fault == "after-throw") throw new InvalidOperationException("mock after exception"); var after = mock.Stored.Reverse().ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal); if (mock.Fault == "missing") after.Remove(after.Keys.First()); if (mock.Fault == "extra") after.Add("test-extra", "x"); if (mock.Fault == "changed") after[after.Keys.First()] += " "; if (mock.Fault == "window-ended") mock.Window["expiresUtc"] = DateTime.UtcNow.AddSeconds(-1).ToString("o"); mock.ReturnedOrder = after.Keys.ToArray(); return Reply(new { data = after }); case "/playerdata/Set": mock.Set++; Check(mock.Login == 1 && mock.Get == 1 && mock.Set == 1, "one Set after baseline"); Check(body.Count == 1 && body["data"] is JObject, "Set data only"); var data = (JObject)body["data"]; Check(data.Count == 55, "55 entries incl unknown keys"); foreach (var p in mock.Expected) Check(data[p.Key]?.Type == JTokenType.String && (string)data[p.Key] == p.Value, "original string " + p.Key); mock.Stored = data.Properties().ToDictionary(p => p.Name, p => (string)p.Value, StringComparer.Ordinal); if (mock.Fault == "set-throw") throw new InvalidOperationException("mock uncaught transport exception"); if (mock.Fault == "unknown") return Reply(new { }, HttpStatusCode.ServiceUnavailable); return Reply(new { }); default: throw new InvalidOperationException("Forbidden request (including PF/102/Delete): " + request.RequestUri); } } } } internal static async Task Run() { assertions = 0; assertionFailures.Clear(); var start = DateTime.UtcNow; var suite = Path.GetFullPath(Import101.Root + "D2-tests/" + start.ToString("yyyyMMddTHHmmssZ") + "-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(suite); var original = Import101.LoadInputs(new Import101.InputPaths()); var cases = new List(); int realDecodes = 0; var names = new[] { "source-hash", "manifest-missing-key", "manifest-duplicate-key", "manifest-value-change", "config-manifest-version", "config-hash", "source-json-duplicate", "source-json-trailing", "window-not-approved", "window-expired", "null-handler", "login-http", "login-protocol", "new-account", "before-http", "nonempty", "candidate-invalid", "save-before", "save-candidate", "claim-existing", "claim-failure", "claim-partial", "claim-race", "window-after-claim", "save-account", "success-real-decode", "unknown", "set-throw", "save-set-result", "after-http", "after-throw", "missing", "extra", "changed", "save-after", "save-verification", "window-ended" }; foreach (var name in names) { var evidence = Path.Combine(suite, name); Directory.CreateDirectory(evidence); var paths = new Import101.InputPaths(); var window = Window(); var mock = new Mock(original.Raw) { Fault = name, Window = window }; var claim = Path.Combine(evidence, "import-101.claimed"); string previousClaim = null; if (name == "claim-existing") { previousClaim = "existing-attempt"; File.WriteAllText(claim, previousClaim); } if (name == "source-hash") { paths.SourcePath = Path.Combine(evidence, "source.json"); File.WriteAllText(paths.SourcePath, "{}"); } if (name.StartsWith("manifest-", StringComparison.Ordinal)) { var values = (JObject)Import101.Parse(File.ReadAllText(paths.ValuesPath)); var entries = (JArray)values["entries"]; if (name == "manifest-missing-key") entries.RemoveAt(0); if (name == "manifest-duplicate-key") entries.Add(entries[0].DeepClone()); if (name == "manifest-value-change") entries[0]["valueSha256"] = "changed"; paths.ValuesPath = Path.Combine(evidence, "values.json"); File.WriteAllText(paths.ValuesPath, values.ToString()); } if (name == "config-manifest-version") { var cfg = (JObject)Import101.Parse(File.ReadAllText(paths.ConfigPath)); cfg["version"] = "wrong"; paths.ConfigPath = Path.Combine(evidence, "config.json"); File.WriteAllText(paths.ConfigPath, cfg.ToString()); } if (name == "config-hash") { paths.ConfigRoot = Path.Combine(evidence, "config-copy"); Directory.CreateDirectory(Path.Combine(paths.ConfigRoot, "cfgData")); var cfg = (JObject)Import101.Parse(File.ReadAllText(paths.ConfigPath)); foreach (var item in (JArray)cfg["items"]) File.Copy((string)item["path"], Path.Combine(paths.ConfigRoot, (string)item["path"])); File.AppendAllText(Path.Combine(paths.ConfigRoot, (string)cfg["items"][0]["path"]), " "); } if (name == "window-not-approved") window["implementationReviewApproved"] = false; if (name == "window-expired") window["expiresUtc"] = DateTime.UtcNow.AddSeconds(-1).ToString("o"); Action save = (path, value) => { var file = Path.GetFileName(path); if ((name == "save-before" && file == "before.json") || (name == "save-candidate" && file == "candidate-schema.json") || (name == "save-set-result" && file == "set-result.json") || (name == "save-after" && file == "after.json") || (name == "save-verification" && file == "verification.json") || (name == "save-account" && file == "account.json") || (name == "claim-failure" && path == claim)) throw new IOException("injected persistence failure: " + file); if (name == "claim-partial" && path == claim) { File.WriteAllText(path, "partial-claim"); throw new IOException("injected partial claim"); } if (name == "claim-race" && path == claim) File.WriteAllText(path, "concurrent-claim"); Import101.Save(path, value); if (name == "window-after-claim" && path == claim) window["expiresUtc"] = DateTime.UtcNow.AddSeconds(-1).ToString("o"); }; Func validate = (session, input) => { if (name == "candidate-invalid") return new PlayerKVSchemaResult(PlayerKVSchemaVerdict.Malformed, new PlayerKVSchemaDiagnostics()); if (name == "success-real-decode") { realDecodes++; Check(session.ServerTimeUtc == new DateTime(2026, 9, 28, 7, 30, 0, DateTimeKind.Utc), "candidate uses mock server time"); return Import101.ValidateCandidate(session, input); } return new PlayerKVSchemaResult(PlayerKVSchemaVerdict.Valid, new PlayerKVSchemaDiagnostics()); }; int beforeAssertions = assertions; string folder = null, error = null; try { if (name.StartsWith("source-json-", StringComparison.Ordinal)) { Import101.Parse(name == "source-json-duplicate" ? "{\"a\":1,\"a\":2}" : "{} {}"); } else folder = await Import101.RunCore(evidence, paths, window, name == "null-handler" ? null : mock.Handler, validate, save); } catch (Exception e) { error = e.GetType().Name + ": " + e.Message; } // Expected importer exceptions must not hide an assertion thrown inside a mock handler. Check(assertionFailures.Count == 0, name + " no swallowed mock assertion"); bool success = name == "success-real-decode"; Check((error == null) == success, name + " expected outcome: " + error); int expectedSet = Array.IndexOf(names, name) >= Array.IndexOf(names, "success-real-decode") ? 1 : 0; Check(mock.Set == expectedSet, name + " Set bound"); if (expectedSet == 1) { Check(mock.Login == 1 && mock.Get == 2, name + " one observation even on failure"); Check(File.Exists(claim), name + " durable claim retained"); var claimHash = Import101.Hash(File.ReadAllBytes(claim)); int total = mock.Login + mock.Get + mock.Set; bool rejected = false; try { await Import101.RunCore(evidence, paths, Window(), mock.Handler, validate, save); } catch (InvalidOperationException) { rejected = true; } Check(rejected && total == mock.Login + mock.Get + mock.Set, name + " replay rejected before network"); Check(Import101.Hash(File.ReadAllBytes(claim)) == claimHash, name + " replay claim unchanged"); } if (name.StartsWith("source-", StringComparison.Ordinal) || name.StartsWith("manifest-", StringComparison.Ordinal) || name.StartsWith("config-", StringComparison.Ordinal) || name is "window-not-approved" or "window-expired" || name == "null-handler" || name == "claim-existing") Check(mock.Login == 0 && mock.Get == 0, name + " local gate"); if (name is "login-http" or "login-protocol" or "new-account") Check(mock.Get == 0, name + " no Get after login refusal"); if (name == "claim-existing") Check(File.ReadAllText(claim) == previousClaim, "existing claim byte content"); if (name == "claim-partial") Check(File.ReadAllText(claim) == "partial-claim", "partial claim retained"); if (name == "claim-race") Check(File.ReadAllText(claim) == "concurrent-claim", "competing claim not overwritten"); if (name == "window-after-claim") Check(File.Exists(claim) && mock.Get == 1, "expired window consumes claim but never Set"); if (name == "save-account") Check(mock.Login == 1 && mock.Get == 0, "account evidence before Get"); if (name is "candidate-invalid" or "save-before" or "save-candidate" or "claim-failure") Check(!File.Exists(claim), name + " no completed claim"); var runs = Path.Combine(evidence, "runs"); var actualFolder = folder ?? (Directory.Exists(runs) ? Directory.GetDirectories(runs).Single() : null); if (name == "save-set-result") Check(File.Exists(Path.Combine(actualFolder, "after.json")), "after persisted despite set log failure"); if (success) { var result = (JObject)Import101.Parse(File.ReadAllText(Path.Combine(folder, "verification.json"))); Check((bool)result["valuesEqual"] && (bool)result["setSuccess"], "positive exact readback"); Check(result["adapterKeyOrder"].Values().SequenceEqual(mock.ReturnedOrder), "unmodified adapter enumeration order"); var decoded = (JObject)Import101.Parse(File.ReadAllText(Path.Combine(folder, "candidate-schema.json"))); Check((string)decoded["verdict"] == "Valid" && (int)decoded["errors"] == 0, "real 55-key candidate valid"); } cases.Add(new { name, pass = true, assertions = assertions - beforeAssertions, mock.Login, mock.Get, mock.Set, evidence, error, realCandidateDecode = success, formalRequests = 0 }); Console.WriteLine("PASS " + name); } Check(realDecodes == 1, "real candidate exercised once"); Import101.Save(Path.Combine(suite, "result.json"), new { success = true, cases, assertions, realDecodes, startUtc = start, endUtc = DateTime.UtcNow, formalRequests = 0, note = "All handlers are in-memory; failures other than success case are injected." }); Console.WriteLine(JsonConvert.SerializeObject(new { success = true, cases = cases.Count, assertions, realDecodes, formalRequests = 0, suite })); } }