保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。 保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。 验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。 包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
137 lines
7.6 KiB
C#
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(); }
|
|
}
|
|
}
|
|
}
|