Files
ft/CodexOutput/upserver/KVGameLoadTests/ServiceHost/C3IdentityCompatibilityTests.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

136 lines
8.0 KiB
C#

using game;
using Newtonsoft.Json.Linq;
using System.Net;
#if UNITY_EDITOR
static partial class Probe
{
sealed class IdentityWireCapture
{
internal readonly List<(string Path, string Body)> Requests = new();
internal HttpStatusCode NextStatus = HttpStatusCode.OK;
internal string NextBody = "{\"Code\":1}";
internal Task<HttpResponseMessage> Respond(string path, string body)
{
Requests.Add((path, body));
var result = new HttpResponseMessage(NextStatus) { Content = new StringContent(NextBody) };
NextStatus = HttpStatusCode.OK; NextBody = "{\"Code\":1}";
return Task.FromResult(result);
}
internal bool TryHandle(HttpRequestMessage request, out Task<HttpResponseMessage> response)
{
response = null;
var path = request.RequestUri.AbsolutePath;
if (!new[] { "/friend/AddFriend", "/friend/DeleteFriend", "/friend/SearchFriend", "/friend/AcceptFriend",
"/friend/GetHomePage", "/friend/GetRecommendedFriends", "/friend/GetInvitedList",
"/friend/IsFriendWaitingForAccept" }.Contains(path)) return false;
Check(request.RequestUri.GetLeftPart(UriPartial.Authority) == "http://s101.local:18080" && request.Method == HttpMethod.Post,
"original friend route uses fixed official origin through in-memory transport");
response = Respond(path, request.Content.ReadAsStringAsync().GetAwaiter().GetResult());
return true;
}
}
public class IdentityPFTransport : System.Reflection.DispatchProxy
{
public Func<string, string, Task<string>> Send;
protected override object Invoke(System.Reflection.MethodInfo method, object[] args)
{
if (method.Name != "FriendRequest") throw new Exception("Unexpected PF fixture transport: " + method.Name);
return Send("/friend/" + args[0], Newtonsoft.Json.JsonConvert.SerializeObject(args[1]));
}
}
sealed class IdentityPFUser : PlayFabUserService { public IdentityPFUser() { UserId = "ABCD1234"; } }
static async Task IdentityCompatibility(IUserService user, ICustomServerMgr server, IdentityWireCapture wire, string other)
{
GContext.container.Values[typeof(PlayerData)] = new PlayerData { lv = 10 };
var friend = new FriendService { userService = user, customServerMgr = server };
friend.FriendList.Add(new LeadboardItemData { playFabId = other });
var initialSDK = Effects.SDK;
JObject Last(string action)
{
var last = wire.Requests[^1];
Check(last.Path == "/friend/" + action, "original route retained: " + action);
return JObject.Parse(last.Body);
}
void Fields(JObject value, params string[] fields) => Check(value.Properties().Select(p => p.Name).OrderBy(x=>x)
.SequenceEqual(fields.OrderBy(x=>x)), "original field names retained, no userId alias added");
wire.NextBody = "{\"Code\":-3,\"Msg\":\"synthetic compatibility failure\"}";
var add = JObject.Parse(await friend.AddFriend(other));
var request = Last("AddFriend"); Fields(request, "MySelf", "MyFriendCount", "FriendPlayFabId");
Fields((JObject)request["MySelf"], "PlayFabId", "DisplayName", "AvatarUrl", "Level");
Check((string)request["MySelf"]["PlayFabId"] == user.UserId && (string)request["FriendPlayFabId"] == other,
"self uses authenticated identity and friend retains the distinct target identity");
Check((int)request["MySelf"]["Level"] == 10 && (int)request["MyFriendCount"] == 1 && (int)add["Code"] == -3,
"original data and server business failure are returned unchanged");
wire.NextBody = "{\"Code\":1,\"Info\":{\"PlayFabId\":\"" + other + "\",\"Level\":12}}";
var search = JObject.Parse(await friend.SearchFriend(other));
request = Last("SearchFriend"); Fields(request, "PlayFabId");
Check((string)request["PlayFabId"] == other && (string)search["Info"]["PlayFabId"] == other,
"target UserID round trips in legacy PlayFabId field without substituting self");
await friend.AcceptFriend(2, user.UserId, other);
request = Last("AcceptFriend"); Fields(request, "Status", "PlayFabId", "FriendPlayFabId");
Check((int)request["Status"] == 2 && (string)request["PlayFabId"] == user.UserId && (string)request["FriendPlayFabId"] == other,
"original accept/ignore status and two identities stay distinct");
await friend.GetHomePage(other, 3);
request = Last("GetHomePage"); Fields(request, "PlayFabId", "PageNo");
Check((string)request["PlayFabId"] == other && (int)request["PageNo"] == 3, "home-page target and page retained");
await friend.GetRecommendedFriends(user.UserId, 4);
request = Last("GetRecommendedFriends"); Fields(request, "PlayFabId", "PageNo");
Check((string)request["PlayFabId"] == user.UserId && (int)request["PageNo"] == 4, "recommendation self identity and page retained");
await friend.IsFriendWaitingForAccept(user.UserId, other);
request = Last("IsFriendWaitingForAccept"); Fields(request, "PlayFabId", "FriendPlayFabId");
Check((string)request["PlayFabId"] == user.UserId && (string)request["FriendPlayFabId"] == other,
"invitation status uses self and target without conversion");
await friend.GetInvitedList();
Check(wire.Requests[^1] == ("/friend/GetInvitedList", "\"\""), "cookie-authenticated original empty body remains unchanged");
wire.NextBody = "{\"Code\":-1}";
Check(!await friend.DeleteFriend(other) && friend.FriendList.Count == 1, "business failure retains original friend list");
request = Last("DeleteFriend"); Fields(request, "FriendPlayFabId");
Check((string)request["FriendPlayFabId"] == other, "delete targets the supplied UserID");
Check(await friend.DeleteFriend(other) && friend.FriendList.Count == 0, "original successful delete updates list");
foreach (var id in new[] { user.UserId, other })
{
var invitation = friend.GetInvitationCodeByPlayFabID(id).Replace("-", "");
Check(friend.DecryptInvitationCodeToUlong(invitation).ToString("X") == id,
"original invitation codec round trips identity string " + id);
}
Check(Effects.SDK == initialSDK, "ordinary friend requests do not call PlayFab SDK");
if (user is KVUserService)
{
wire.NextStatus = HttpStatusCode.InternalServerError;
Check(await friend.SearchFriend(other) == null, "HTTP failure retains original null result, no fake success");
Check(user.IsAuthorized, "ordinary HTTP 500 does not invent logout");
}
Pass("identity-legacy-fields-real-friend-service-" + user.UserId);
}
static async Task IdentityPFCompatibility()
{
var wire = new IdentityWireCapture();
var server = System.Reflection.DispatchProxy.Create<ICustomServerMgr, IdentityPFTransport>();
((IdentityPFTransport)(object)server).Send = async (path, body) =>
await (await wire.Respond(path, body)).Content.ReadAsStringAsync();
await IdentityCompatibility(new IdentityPFUser(), server, wire, "FE123456");
}
}
#endif
// Only unused UI/config dependencies of the linked real FriendService are fixtures.
namespace game
{
public class FriendAddItemData { }
public class FriendAddPanelBackendOperation
{
public void StartInvite(string id, Action<FriendAddPanelBackendOperation, string, int> callback) =>
throw new NotSupportedException("UI operation is outside this protocol test.");
public void Clear() { }
}
public class LeadboardItemData { public string playFabId, displayName, avatarUrl; public DateTime? LastLogin; public int value, position, rank;
public void SetData(PlayFab.ClientModels.PlayerLeaderboardEntry entry) => throw new NotSupportedException("Leaderboard reads are outside this statistics-write test."); }
}