repo(viewer): add UDID lookup, anonymous register, Steam link helpers
Extracts the default-loadout body into a private BuildDefaultViewer helper shared by the existing Steam-import path and a new RegisterAnonymousViewer for /tool/signup. LinkSteamToViewer is the seam SteamSessionAuthenticationHandler will call on first-Steam-touch of a UDID-keyed viewer. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,10 @@ public interface IViewerRepository
|
|||||||
Task<Models.Viewer?> GetViewerBySocialConnection(SocialAccountType accountType, ulong socialId);
|
Task<Models.Viewer?> GetViewerBySocialConnection(SocialAccountType accountType, ulong socialId);
|
||||||
Task<Models.Viewer?> GetViewerWithSocials(long id);
|
Task<Models.Viewer?> GetViewerWithSocials(long id);
|
||||||
Task<Models.Viewer?> GetViewerByShortUdid(long shortUdid);
|
Task<Models.Viewer?> GetViewerByShortUdid(long shortUdid);
|
||||||
|
Task<Models.Viewer?> GetViewerByUdid(Guid udid);
|
||||||
|
|
||||||
Task<Models.Viewer> RegisterViewer(string displayName, SocialAccountType socialType,
|
Task<Models.Viewer> RegisterViewer(string displayName, SocialAccountType socialType,
|
||||||
ulong socialAccountIdentifier, ulong? shortUdid = null);
|
ulong socialAccountIdentifier, ulong? shortUdid = null);
|
||||||
|
Task<Models.Viewer> RegisterAnonymousViewer(Guid udid);
|
||||||
|
Task LinkSteamToViewer(long viewerId, ulong steamId);
|
||||||
}
|
}
|
||||||
@@ -70,6 +70,58 @@ public class ViewerRepository : IViewerRepository
|
|||||||
|
|
||||||
public async Task<Models.Viewer> RegisterViewer(string displayName, SocialAccountType socialType,
|
public async Task<Models.Viewer> RegisterViewer(string displayName, SocialAccountType socialType,
|
||||||
ulong socialAccountIdentifier, ulong? shortUdid = null)
|
ulong socialAccountIdentifier, ulong? shortUdid = null)
|
||||||
|
{
|
||||||
|
var viewer = await BuildDefaultViewer(displayName);
|
||||||
|
viewer.SocialAccountConnections.Add(new SocialAccountConnection
|
||||||
|
{
|
||||||
|
AccountId = socialAccountIdentifier,
|
||||||
|
AccountType = socialType
|
||||||
|
});
|
||||||
|
_dbContext.Set<Models.Viewer>().Add(viewer);
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
return viewer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Models.Viewer?> GetViewerByUdid(Guid udid)
|
||||||
|
{
|
||||||
|
if (udid == Guid.Empty) return null;
|
||||||
|
return await _dbContext.Set<Models.Viewer>()
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(v => v.Udid == udid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Models.Viewer> RegisterAnonymousViewer(Guid udid)
|
||||||
|
{
|
||||||
|
if (udid == Guid.Empty)
|
||||||
|
throw new InvalidOperationException("Cannot register viewer for empty UDID.");
|
||||||
|
|
||||||
|
var viewer = await BuildDefaultViewer("Player");
|
||||||
|
viewer.Udid = udid;
|
||||||
|
_dbContext.Set<Models.Viewer>().Add(viewer);
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
return viewer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LinkSteamToViewer(long viewerId, ulong steamId)
|
||||||
|
{
|
||||||
|
var viewer = await _dbContext.Set<Models.Viewer>()
|
||||||
|
.Include(v => v.SocialAccountConnections)
|
||||||
|
.FirstOrDefaultAsync(v => v.Id == viewerId)
|
||||||
|
?? throw new InvalidOperationException($"Viewer {viewerId} not found for Steam link.");
|
||||||
|
|
||||||
|
bool alreadyLinked = viewer.SocialAccountConnections.Any(sac =>
|
||||||
|
sac.AccountType == SocialAccountType.Steam && sac.AccountId == steamId);
|
||||||
|
if (alreadyLinked) return;
|
||||||
|
|
||||||
|
viewer.SocialAccountConnections.Add(new SocialAccountConnection
|
||||||
|
{
|
||||||
|
AccountId = steamId,
|
||||||
|
AccountType = SocialAccountType.Steam
|
||||||
|
});
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Models.Viewer> BuildDefaultViewer(string displayName)
|
||||||
{
|
{
|
||||||
Models.Viewer viewer = new Models.Viewer
|
Models.Viewer viewer = new Models.Viewer
|
||||||
{
|
{
|
||||||
@@ -79,12 +131,6 @@ public class ViewerRepository : IViewerRepository
|
|||||||
var grants = _config.Get<DefaultGrantsConfig>();
|
var grants = _config.Get<DefaultGrantsConfig>();
|
||||||
var loadout = _config.Get<DefaultLoadoutConfig>();
|
var loadout = _config.Get<DefaultLoadoutConfig>();
|
||||||
|
|
||||||
viewer.SocialAccountConnections.Add(new SocialAccountConnection
|
|
||||||
{
|
|
||||||
AccountId = socialAccountIdentifier,
|
|
||||||
AccountType = socialType
|
|
||||||
});
|
|
||||||
|
|
||||||
viewer.Info.MaxFriends = player.MaxFriends;
|
viewer.Info.MaxFriends = player.MaxFriends;
|
||||||
viewer.Info.CountryCode = "KOR";
|
viewer.Info.CountryCode = "KOR";
|
||||||
viewer.Info.BirthDate = DateTime.UtcNow;
|
viewer.Info.BirthDate = DateTime.UtcNow;
|
||||||
@@ -124,8 +170,6 @@ public class ViewerRepository : IViewerRepository
|
|||||||
if (defaultEmblem is not null) viewer.Emblems.Add(defaultEmblem);
|
if (defaultEmblem is not null) viewer.Emblems.Add(defaultEmblem);
|
||||||
if (defaultBg is not null) viewer.MyPageBackgrounds.Add(defaultBg);
|
if (defaultBg is not null) viewer.MyPageBackgrounds.Add(defaultBg);
|
||||||
|
|
||||||
// Grant one of each class's default leader skin. Filter out the synthetic placeholders
|
|
||||||
// (Id=0) and dedupe — skins are many-to-many via SleeveEntryViewer-style join.
|
|
||||||
var grantedSkins = viewer.Classes
|
var grantedSkins = viewer.Classes
|
||||||
.Select(vcd => vcd.LeaderSkin)
|
.Select(vcd => vcd.LeaderSkin)
|
||||||
.Where(s => s.Id != 0)
|
.Where(s => s.Id != 0)
|
||||||
@@ -133,8 +177,6 @@ public class ViewerRepository : IViewerRepository
|
|||||||
.ToList();
|
.ToList();
|
||||||
viewer.LeaderSkins.AddRange(grantedSkins);
|
viewer.LeaderSkins.AddRange(grantedSkins);
|
||||||
|
|
||||||
_dbContext.Set<Models.Viewer>().Add(viewer);
|
|
||||||
await _dbContext.SaveChangesAsync();
|
|
||||||
return viewer;
|
return viewer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,4 +71,99 @@ public class ViewerRepositoryTests
|
|||||||
Assert.That(viewer.LeaderSkins, Is.Not.Empty,
|
Assert.That(viewer.LeaderSkins, Is.Not.Empty,
|
||||||
"Viewer should own at least one leader skin from class defaults.");
|
"Viewer should own at least one leader skin from class defaults.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task RegisterAnonymousViewer_creates_viewer_with_udid_and_no_socials()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
var udid = Guid.NewGuid();
|
||||||
|
|
||||||
|
long viewerId;
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var repo = scope.ServiceProvider.GetRequiredService<IViewerRepository>();
|
||||||
|
var v = await repo.RegisterAnonymousViewer(udid);
|
||||||
|
viewerId = v.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var verifyScope = factory.Services.CreateScope();
|
||||||
|
var db = verifyScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||||
|
var loaded = await db.Viewers
|
||||||
|
.Include(v => v.Classes)
|
||||||
|
.Include(v => v.SocialAccountConnections)
|
||||||
|
.FirstAsync(v => v.Id == viewerId);
|
||||||
|
|
||||||
|
Assert.That(loaded.Udid, Is.EqualTo(udid));
|
||||||
|
Assert.That(loaded.SocialAccountConnections, Is.Empty);
|
||||||
|
Assert.That(loaded.Classes, Is.Not.Empty,
|
||||||
|
"Default-loadout body should populate Classes (smoke-test the shared BuildDefaultViewer helper).");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task RegisterAnonymousViewer_with_empty_udid_throws()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
using var scope = factory.Services.CreateScope();
|
||||||
|
var repo = scope.ServiceProvider.GetRequiredService<IViewerRepository>();
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||||
|
await repo.RegisterAnonymousViewer(Guid.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetViewerByUdid_returns_viewer_or_null()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
var udid = Guid.NewGuid();
|
||||||
|
|
||||||
|
long createdId;
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var repo = scope.ServiceProvider.GetRequiredService<IViewerRepository>();
|
||||||
|
createdId = (await repo.RegisterAnonymousViewer(udid)).Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var repo = scope.ServiceProvider.GetRequiredService<IViewerRepository>();
|
||||||
|
var hit = await repo.GetViewerByUdid(udid);
|
||||||
|
var miss = await repo.GetViewerByUdid(Guid.NewGuid());
|
||||||
|
|
||||||
|
Assert.That(hit, Is.Not.Null);
|
||||||
|
Assert.That(hit!.Id, Is.EqualTo(createdId));
|
||||||
|
Assert.That(miss, Is.Null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task LinkSteamToViewer_appends_steam_social_connection()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
var udid = Guid.NewGuid();
|
||||||
|
const ulong steamId = 76_561_198_900_000_001UL;
|
||||||
|
|
||||||
|
long viewerId;
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var repo = scope.ServiceProvider.GetRequiredService<IViewerRepository>();
|
||||||
|
viewerId = (await repo.RegisterAnonymousViewer(udid)).Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var repo = scope.ServiceProvider.GetRequiredService<IViewerRepository>();
|
||||||
|
await repo.LinkSteamToViewer(viewerId, steamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var verifyScope = factory.Services.CreateScope();
|
||||||
|
var db = verifyScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||||
|
var loaded = await db.Viewers
|
||||||
|
.Include(v => v.SocialAccountConnections)
|
||||||
|
.FirstAsync(v => v.Id == viewerId);
|
||||||
|
|
||||||
|
Assert.That(loaded.SocialAccountConnections, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(loaded.SocialAccountConnections[0].AccountType,
|
||||||
|
Is.EqualTo(SocialAccountType.Steam));
|
||||||
|
Assert.That(loaded.SocialAccountConnections[0].AccountId, Is.EqualTo(steamId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user