feat(replay): add ViewerBattleHistory entity + migration

New table backs /replay/info; composite PK (ViewerId, BattleId), index on
(ViewerId, CreateTime) for the newest-first list query. 50-row per-viewer
retention enforced by BattleHistoryWriter (next commit).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-10 07:32:05 -04:00
parent 2d65fcd91c
commit 0bb0f46abc
6 changed files with 4916 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SVSim.Database;
using SVSim.Database.Models;
using SVSim.UnitTests.Infrastructure;
namespace SVSim.UnitTests.Persistence;
public class ViewerBattleHistoryPersistenceTests
{
[Test]
public async Task ViewerBattleHistory_round_trips_composite_PK_row()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync(steamId: 76_561_198_000_000_001UL);
using (var seedScope = factory.Services.CreateScope())
{
var db = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
db.ViewerBattleHistories.Add(new ViewerBattleHistory
{
ViewerId = viewerId,
BattleId = 234_471_983_876L,
BattleType = 4,
DeckFormat = 2,
TwoPickType = 0,
IsLimitTurn = 0,
SelfClassId = 8,
SelfSubClassId = 0,
SelfCharaId = 8,
SelfRotationId = "0",
OpponentClassId = 5,
OpponentSubClassId = 0,
OpponentCharaId = 805,
OpponentName = "Foo",
OpponentCountryCode = "",
OpponentEmblemId = 721_341_010L,
OpponentDegreeId = 120_023L,
OpponentRotationId = "0",
IsWin = false,
BattleStartTime = new DateTime(2026, 6, 4, 17, 13, 13, DateTimeKind.Utc),
CreateTime = new DateTime(2026, 6, 4, 17, 16, 6, DateTimeKind.Utc),
});
await db.SaveChangesAsync();
}
using (var readScope = factory.Services.CreateScope())
{
var db = readScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var row = await db.ViewerBattleHistories.SingleAsync(h =>
h.ViewerId == viewerId && h.BattleId == 234_471_983_876L);
Assert.That(row.OpponentName, Is.EqualTo("Foo"));
Assert.That(row.OpponentEmblemId, Is.EqualTo(721_341_010L));
}
}
[Test]
public async Task ViewerBattleHistory_composite_PK_rejects_duplicate_battle_id_for_same_viewer()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync(steamId: 76_561_198_000_000_001UL);
using (var seedScope = factory.Services.CreateScope())
{
var db = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
db.ViewerBattleHistories.Add(NewRow(viewerId, battleId: 1L));
await db.SaveChangesAsync();
}
using (var dupScope = factory.Services.CreateScope())
{
var db = dupScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
db.ViewerBattleHistories.Add(NewRow(viewerId, battleId: 1L));
Assert.ThrowsAsync<DbUpdateException>(async () => await db.SaveChangesAsync());
}
}
private static ViewerBattleHistory NewRow(long viewerId, long battleId) => new()
{
ViewerId = viewerId,
BattleId = battleId,
SelfRotationId = "0",
OpponentName = "",
OpponentCountryCode = "",
OpponentRotationId = "0",
BattleStartTime = DateTime.UtcNow,
CreateTime = DateTime.UtcNow,
};
}