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 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 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> 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(); ((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 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."); } }