Files
ft/CodexOutput/upserver/KVGameLoadTests/CP2AExistingArchiveTests.cs
Liubing\LB 6813da149a feat(upserver): 保存M2客户端KV迁移至本人统计写检查点
保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。
保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。

验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。
包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
2026-09-30 19:19:05 +08:00

137 lines
7.6 KiB
C#

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<PlayerKVIdentityChannel>()),
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<PlayerKVIdentityChannel>()),
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<CookieContainer, HttpMessageHandler> 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<CookieContainer, HttpMessageHandler> 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<string, string>() : ModernRaw()
})));
});
};
Func<CookieContainer, HttpMessageHandler> 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<PlayerKVIdentityChannel>()),
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(); }
}
}
}