fix(check): emit rewrite_viewer_id when UDID and Steam viewers disagree

Wipe-and-resignup left the client stuck with the blank V_new's id in
Certification.ViewerId. /tool/signup is anonymous, so it can't see the
Steam ticket and creates a fresh anonymous viewer keyed on the new UDID;
the Steam handler on the next request resolves to V_old and serves its
data, but no normal-response hook overwrites Certification.ViewerId.
GameStart now compares the UDID-keyed viewer to the auth-resolved one
and emits rewrite_viewer_id when they differ, which Cute/GameStartCheckTask
writes back into Certification.ViewerId.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-01 01:49:56 -04:00
parent cc32223d7d
commit e7dac31d52
2 changed files with 75 additions and 0 deletions

View File

@@ -1,6 +1,9 @@
using System.Net;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using SVSim.Database.Repositories.Viewer;
using SVSim.EmulatedEntrypoint.Services;
using SVSim.UnitTests.Infrastructure;
namespace SVSim.UnitTests.Controllers;
@@ -103,6 +106,58 @@ public class CheckControllerTests
"account_delete_reservation_status must NOT be present — presence triggers client behavior.");
}
[Test]
public async Task GameStart_emits_rewrite_viewer_id_when_udid_keyed_viewer_differs_from_authed_viewer()
{
// Reproduces the wipe-and-resignup scenario: the client wiped local prefs, hit
// /tool/signup with a fresh UDID (creating a blank anonymous viewer V_new), then
// hit /check/game_start carrying the same Steam ticket. The Steam handler resolves
// to V_old (the original viewer with the Steam link), but the client still thinks
// it is V_new from the signup response. rewrite_viewer_id is the documented client
// hook for correcting Certification.ViewerId — see Cute/GameStartCheckTask.cs:113.
using var factory = new SVSimTestFactory();
long oldViewerId = await factory.SeedViewerAsync();
// V_new: blank-named anonymous viewer keyed by a fresh UDID, as RegisterAnonymousViewer
// would have produced inside /tool/signup. Resolved via the real repo so the row matches
// what production lays down.
Guid freshUdid = Guid.NewGuid();
long newViewerId;
using (var scope = factory.Services.CreateScope())
{
var repo = scope.ServiceProvider.GetRequiredService<IViewerRepository>();
var v = await repo.RegisterAnonymousViewer(freshUdid);
newViewerId = v.Id;
}
// SID→UDID mapping for U_new, so HttpContext.GetUdid() resolves to it inside the
// controller (same setup MakeClientWithUdid in ToolControllerTests uses).
const string sid = "wipe-resignup-test-sid";
using (var scope = factory.Services.CreateScope())
{
var session = scope.ServiceProvider.GetRequiredService<ShadowverseSessionService>();
session.StoreUdidForSessionId(sid, freshUdid);
}
using var client = factory.CreateAuthenticatedClient(oldViewerId);
client.DefaultRequestHeaders.Add("SID", sid);
var response = await client.PostAsync("/check/game_start",
new StringContent(GameStartRequestJson, Encoding.UTF8, "application/json"));
var body = await response.Content.ReadAsStringAsync();
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
using var doc = JsonDocument.Parse(body);
var root = doc.RootElement;
Assert.That(root.TryGetProperty("rewrite_viewer_id", out var rewrite), Is.True,
$"rewrite_viewer_id must be present when UDID-keyed viewer ({newViewerId}) differs from auth-resolved viewer ({oldViewerId}). " +
"Without it the client stays stuck on the wrong Certification.ViewerId after a wipe-and-resignup. Body: " + body);
Assert.That(rewrite.GetInt64(), Is.EqualTo(oldViewerId),
"rewrite_viewer_id must point to the auth-resolved (Steam-linked) viewer, not the UDID-keyed anonymous one.");
}
[Test]
public async Task GameStart_with_no_viewer_returns_401()
{