import_viewer: fix counter upsert race + add explicit slot field

Pre-materialize ViewerEventCounters before the mission loop so in-flight
Adds are visible to subsequent iterations sharing the same (EventKey, Period);
add optional ImportMission.Slot field so callers can supply weekly slot identity
(2 or 3) rather than relying on the LotType-derived default of 1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-23 19:47:40 -04:00
parent 22778b65fd
commit c253930d59
3 changed files with 129 additions and 14 deletions

View File

@@ -585,4 +585,96 @@ public class AdminControllerTests
Assert.That(verifyDb3.ViewerMissions.Count(m => m.MissionCatalogId == 9002), Is.EqualTo(1));
Assert.That(verifyDb3.ViewerEventCounters.Any(), Is.False);
}
[Test]
public async Task ImportViewer_Missions_TwoMissionsSharingEventType_SingleCounterUpsert()
{
// Two weekly missions (LotType=2) share the same EventType — same (EventKey, WeekKey)
// period — but occupy different slots (1 and 2 via explicit slot override) so the slot
// unique-index is satisfied. Without the pre-materialized counterCache the second DB read
// won't see the first mission's pending Add → duplicate insert / unique-constraint failure.
// With the fix both missions upsert the SAME counter, resulting in exactly one row.
using var factory = new SVSimTestFactory();
using (var seedScope = factory.Services.CreateScope())
{
var seedDb = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
seedDb.MissionCatalog.AddRange(
new MissionCatalogEntry { Id = 9003, LotType = 2, EventType = "battle_win_total", EventArg = null },
new MissionCatalogEntry { Id = 9004, LotType = 2, EventType = "battle_win_total", EventArg = null });
await seedDb.SaveChangesAsync();
}
var client = factory.CreateClient();
ulong steamId = 70000000000000005UL;
var resp = await client.PostAsJsonAsync("/admin/import_viewer", new ImportViewerRequest
{
SteamId = steamId,
Missions = new List<ImportMission>
{
new() { MissionId = 9003, MissionStatus = 1, TotalCount = 10, Slot = 1 },
new() { MissionId = 9004, MissionStatus = 1, TotalCount = 10, Slot = 2 },
}
});
resp.EnsureSuccessStatusCode();
var body = await resp.Content.ReadFromJsonAsync<ImportViewerResponse>(JsonOptions);
Assert.That(body!.SkippedMissionCount, Is.EqualTo(0));
Assert.That(body.SkippedMissionCounterCount, Is.EqualTo(0));
using var verifyScope = factory.Services.CreateScope();
var verifyDb = verifyScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var viewerId = await verifyDb.Viewers
.Where(v => v.SocialAccountConnections
.Any(s => s.AccountType == SocialAccountType.Steam && s.AccountId == steamId))
.Select(v => v.Id).SingleAsync();
// Must be exactly ONE counter row for (EventKey="battle_win_total"), not two.
Assert.That(
verifyDb.ViewerEventCounters.Count(c => c.ViewerId == viewerId && c.EventKey == "battle_win_total"),
Is.EqualTo(1));
Assert.That(
verifyDb.ViewerEventCounters
.Single(c => c.ViewerId == viewerId && c.EventKey == "battle_win_total").Count,
Is.EqualTo(10));
}
[Test]
public async Task ImportViewer_Missions_ExplicitSlotRoundTrips()
{
// A weekly catalog mission (LotType=2) posted with an explicit slot=3 must persist Slot=3,
// not fall back to the LotType-derived default of 1.
using var factory = new SVSimTestFactory();
using (var seedScope = factory.Services.CreateScope())
{
var seedDb = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
seedDb.MissionCatalog.Add(new MissionCatalogEntry
{
Id = 9005, LotType = 2 /* weekly */,
EventType = null, EventArg = null
});
await seedDb.SaveChangesAsync();
}
var client = factory.CreateClient();
ulong steamId = 70000000000000006UL;
var resp = await client.PostAsJsonAsync("/admin/import_viewer", new ImportViewerRequest
{
SteamId = steamId,
Missions = new List<ImportMission>
{
new() { MissionId = 9005, MissionStatus = 1, TotalCount = 0, Slot = 3 }
}
});
resp.EnsureSuccessStatusCode();
using var verifyScope = factory.Services.CreateScope();
var verifyDb = verifyScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var viewerId = await verifyDb.Viewers
.Where(v => v.SocialAccountConnections
.Any(s => s.AccountType == SocialAccountType.Steam && s.AccountId == steamId))
.Select(v => v.Id).SingleAsync();
var mission = await verifyDb.ViewerMissions
.SingleAsync(m => m.ViewerId == viewerId && m.MissionCatalogId == 9005);
Assert.That(mission.Slot, Is.EqualTo(3));
}
}