feat(friend): bulk apply ops + IPlayedTogetherWriter with retention cap

Implements RejectAllAppliesAsync, CancelAllAppliesAsync (ExecuteDelete bulk
deletes on incoming/outgoing applies respectively) and RecordAsync (upsert
played-together row with 50-row per-viewer retention eviction). 4 new tests
added; all 1186 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-09 22:04:04 -04:00
parent 17591a6ebd
commit b5b4781693
2 changed files with 159 additions and 6 deletions

View File

@@ -535,4 +535,108 @@ public class FriendServiceTests
using var verifyScope = factory.Services.CreateScope();
Assert.That(await Ctx(verifyScope).ViewerFriends.CountAsync(), Is.EqualTo(0));
}
[Test]
public async Task RejectAllAppliesAsync_deletes_only_incoming_for_caller()
{
using var factory = new SVSimTestFactory();
long me = await SeedViewer(factory, 76_561_198_000_002_001UL);
long other = await SeedViewer(factory, 76_561_198_000_002_002UL);
using (var scope = factory.Services.CreateScope())
{
var ctx = Ctx(scope);
ctx.ViewerFriendApplies.Add(new ViewerFriendApply { FromViewerId = other, ToViewerId = me, CreatedAt = DateTime.UtcNow });
ctx.ViewerFriendApplies.Add(new ViewerFriendApply { FromViewerId = me, ToViewerId = other, CreatedAt = DateTime.UtcNow });
await ctx.SaveChangesAsync();
}
var svc = Service(factory, out var scope2);
using (scope2) await svc.RejectAllAppliesAsync(me, default);
using var verifyScope = factory.Services.CreateScope();
var remaining = await Ctx(verifyScope).ViewerFriendApplies.AsNoTracking().ToListAsync();
Assert.That(remaining, Has.Count.EqualTo(1));
Assert.That(remaining[0].FromViewerId, Is.EqualTo(me), "Outgoing must survive");
}
[Test]
public async Task CancelAllAppliesAsync_deletes_only_outgoing_for_caller()
{
using var factory = new SVSimTestFactory();
long me = await SeedViewer(factory, 76_561_198_000_002_003UL);
long other = await SeedViewer(factory, 76_561_198_000_002_004UL);
using (var scope = factory.Services.CreateScope())
{
var ctx = Ctx(scope);
ctx.ViewerFriendApplies.Add(new ViewerFriendApply { FromViewerId = me, ToViewerId = other, CreatedAt = DateTime.UtcNow });
ctx.ViewerFriendApplies.Add(new ViewerFriendApply { FromViewerId = other, ToViewerId = me, CreatedAt = DateTime.UtcNow });
await ctx.SaveChangesAsync();
}
var svc = Service(factory, out var scope2);
using (scope2) await svc.CancelAllAppliesAsync(me, default);
using var verifyScope = factory.Services.CreateScope();
var remaining = await Ctx(verifyScope).ViewerFriendApplies.AsNoTracking().ToListAsync();
Assert.That(remaining, Has.Count.EqualTo(1));
Assert.That(remaining[0].ToViewerId, Is.EqualTo(me), "Incoming must survive");
}
[Test]
public async Task RecordAsync_upserts_PlayedAt_for_existing_pair()
{
using var factory = new SVSimTestFactory();
long me = await SeedViewer(factory, 76_561_198_000_002_005UL);
long opp = await SeedViewer(factory, 76_561_198_000_002_006UL);
var ctxFactory = factory.Services;
IPlayedTogetherWriter writer;
using (var scope = ctxFactory.CreateScope())
{
writer = scope.ServiceProvider.GetRequiredService<IPlayedTogetherWriter>();
await writer.RecordAsync(me, opp, new BattleParticipationContext(1, 2, 3, 4), default);
}
DateTime firstTimestamp;
using (var scope = ctxFactory.CreateScope())
{
firstTimestamp = (await Ctx(scope).ViewerPlayedTogethers.AsNoTracking()
.FirstAsync(p => p.OwnerViewerId == me && p.OpponentViewerId == opp)).PlayedAt;
}
// Wait a tick, record again.
await Task.Delay(20);
using (var scope = ctxFactory.CreateScope())
{
writer = scope.ServiceProvider.GetRequiredService<IPlayedTogetherWriter>();
await writer.RecordAsync(me, opp, new BattleParticipationContext(5, 6, 7, 8), default);
}
using var verifyScope = ctxFactory.CreateScope();
var rows = await Ctx(verifyScope).ViewerPlayedTogethers.AsNoTracking()
.Where(p => p.OwnerViewerId == me).ToListAsync();
Assert.That(rows, Has.Count.EqualTo(1), "Upsert — no duplicate row");
Assert.That(rows[0].PlayedAt, Is.GreaterThan(firstTimestamp));
Assert.That(rows[0].PlayedMode, Is.EqualTo(5), "Latest context wins");
Assert.That(rows[0].BattleType, Is.EqualTo(6));
Assert.That(rows[0].DeckFormat, Is.EqualTo(7));
Assert.That(rows[0].TwoPickType, Is.EqualTo(8));
}
[Test]
public async Task RecordAsync_no_op_when_owner_equals_opponent()
{
using var factory = new SVSimTestFactory();
long me = await SeedViewer(factory, 76_561_198_000_002_007UL);
using (var scope = factory.Services.CreateScope())
{
var writer = scope.ServiceProvider.GetRequiredService<IPlayedTogetherWriter>();
await writer.RecordAsync(me, me, new BattleParticipationContext(0, 0, 0, 0), default);
}
using var verifyScope = factory.Services.CreateScope();
Assert.That(await Ctx(verifyScope).ViewerPlayedTogethers.CountAsync(), Is.EqualTo(0));
}
}