using System.Net; using System.Net.Http; using System.Text; using game.Account; using game.PlayerDataStorage; static class ReviewTests { static async Task Main() { try { await QueuedRefreshCloseReturnsStructuredResult().WaitAsync(TimeSpan.FromSeconds(5)); Console.WriteLine("PASS queued-refresh-close-returns-structured-result"); } catch (Exception ex) { Console.WriteLine($"FAIL queued-refresh-close-returns-structured-result {ex.GetType().Name}: {ex.Message}"); Environment.ExitCode = 1; } } static async Task QueuedRefreshCloseReturnsStructuredResult() { var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var handler = new BlockingMutationHandler(entered, release); var session = await Login("101"); var opened = await PlayerKVSessionOwner.CreateAsync( session, Root(), "101", new Uri("http://unit.test/"), 1, epoch => epoch == 1, default, _ => handler); Check(opened.Success, "owner open failed"); opened.Owner.Update("review", "value"); var flush = opened.Owner.FlushAsync(); await entered.Task; var refresh = opened.Owner.RefreshAsync(); var close = opened.Owner.CloseAsync(); release.TrySetResult(true); PlayerKVResult refreshResult; try { refreshResult = await refresh; } catch (Exception ex) { throw new Exception("RefreshAsync leaked an exception instead of returning PlayerKVResult", ex); } Check(!refreshResult.Success, "refresh should not succeed after owner close"); await flush; await close; session.Dispose(); } static async Task Login(string id) { var session = new S101AccountSession(new Uri("http://unit.test/"), jar => new DelegateHandler(_ => { jar.Add(new Uri("http://unit.test/"), new Cookie("sid", id)); return Json(HttpStatusCode.OK, "{\"state\":0,\"internalId\":1,\"newCreate\":false,\"createTime\":\"2026-01-01T00:00:00Z\",\"svrTime\":\"2026-01-01T00:00:00Z\"}"); })); Check((await session.LoginAsync(id)).Success, "login failed"); return session; } static string Root() { var path = Path.Combine(Path.GetTempPath(), "kv3b-review-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(path); return path; } static HttpResponseMessage Json(HttpStatusCode code, string json) => new(code) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; static void Check(bool value, string message) { if (!value) throw new Exception(message); } sealed class DelegateHandler : HttpMessageHandler { readonly Func action; internal DelegateHandler(Func action) => this.action = action; protected override Task SendAsync(HttpRequestMessage request, CancellationToken token) => Task.FromResult(action(request)); } sealed class BlockingMutationHandler : HttpMessageHandler { readonly TaskCompletionSource entered; readonly TaskCompletionSource release; internal BlockingMutationHandler(TaskCompletionSource entered, TaskCompletionSource release) { this.entered = entered; this.release = release; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken token) { if (request.RequestUri.AbsolutePath.EndsWith("/Get")) return Json(HttpStatusCode.OK, "{\"data\":{}}"); entered.TrySetResult(true); await release.Task; return Json(HttpStatusCode.OK, "{}"); } } }