using System.Net; using System.Net.Http.Json; using System.Text.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using SVSim.Database; using SVSim.Database.Enums; using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Admin; using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Admin; using SVSim.UnitTests.Infrastructure; namespace SVSim.UnitTests.Controllers; /// /// End-to-end coverage for /admin/import_viewer. The endpoint is [AllowAnonymous] so /// these tests don't need to seed a viewer first; the fresh-user path exercises the just-fixed /// nav-graph NRE inside ViewerRepository.RegisterViewer, and the existing-user path /// exercises the owned-type lookup used to dedupe by Steam id. /// public class AdminControllerTests { private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; [Test] public async Task ImportViewer_fresh_user_creates_viewer_and_returns_ids() { using var factory = new SVSimTestFactory(); using var client = factory.CreateClient(); var response = await client.PostAsJsonAsync("/admin/import_viewer", new ImportViewerRequest { SteamId = 76_561_198_222_333_444UL, DisplayName = "Fresh User", CountryCode = "USA", TutorialState = 100, Currency = new ImportCurrency { Crystals = 12345 } }); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), await response.Content.ReadAsStringAsync()); var body = await response.Content.ReadFromJsonAsync(JsonOptions); Assert.That(body, Is.Not.Null); Assert.That(body!.ViewerId, Is.GreaterThan(0), "RegisterViewer must persist and return a non-zero id."); Assert.That(body.WasCreated, Is.True); using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var stored = await db.Viewers .Include(v => v.SocialAccountConnections) .Include(v => v.Currency) .Include(v => v.Info) .FirstAsync(v => v.Id == body.ViewerId); Assert.That(stored.DisplayName, Is.EqualTo("Fresh User")); Assert.That(stored.Currency.Crystals, Is.EqualTo(12345UL), "ImportViewer should overwrite the seed-config crystal default with the requested value."); Assert.That(stored.Info.CountryCode, Is.EqualTo("USA")); Assert.That(stored.SocialAccountConnections.Count, Is.EqualTo(1)); Assert.That(stored.SocialAccountConnections[0].AccountId, Is.EqualTo(76_561_198_222_333_444UL)); Assert.That(stored.SocialAccountConnections[0].AccountType, Is.EqualTo(SocialAccountType.Steam)); } [Test] public async Task ImportViewer_existing_user_updates_in_place() { using var factory = new SVSimTestFactory(); const ulong steamId = 76_561_198_555_666_777UL; long seededId = await factory.SeedViewerAsync(steamId: steamId, displayName: "Original Name"); using var client = factory.CreateClient(); var response = await client.PostAsJsonAsync("/admin/import_viewer", new ImportViewerRequest { SteamId = steamId, DisplayName = "Updated Name", CountryCode = "JPN" }); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), await response.Content.ReadAsStringAsync()); var body = await response.Content.ReadFromJsonAsync(JsonOptions); Assert.That(body, Is.Not.Null); Assert.That(body!.ViewerId, Is.EqualTo(seededId), "Re-importing the same SteamId must reuse the existing viewer row, not create a new one."); Assert.That(body.WasCreated, Is.False); using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var stored = await db.Viewers .Include(v => v.Info) .FirstAsync(v => v.Id == seededId); Assert.That(stored.DisplayName, Is.EqualTo("Updated Name")); Assert.That(stored.Info.CountryCode, Is.EqualTo("JPN")); var viewerCount = await db.Viewers.CountAsync(v => v.SocialAccountConnections.Any(s => s.AccountType == SocialAccountType.Steam && s.AccountId == steamId)); Assert.That(viewerCount, Is.EqualTo(1), "Owned-type dedup must not produce a second row."); } [Test] public async Task ImportViewer_missing_steam_id_returns_400() { using var factory = new SVSimTestFactory(); using var client = factory.CreateClient(); var response = await client.PostAsJsonAsync("/admin/import_viewer", new ImportViewerRequest { SteamId = 0, DisplayName = "No Steam" }); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); } [Test] public async Task ImportViewer_imports_owned_cards_and_skips_unknown() { using var factory = new SVSimTestFactory(); using var client = factory.CreateClient(); // 10001001 is in the minimal test card set; 99999999 is not. var response = await client.PostAsJsonAsync("/admin/import_viewer", new ImportViewerRequest { SteamId = 76_561_198_111_222_333UL, OwnedCards = new List { new() { CardId = 10001001L, Count = 2, IsProtected = true }, new() { CardId = 99999999L, Count = 1, IsProtected = false }, } }); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), await response.Content.ReadAsStringAsync()); var body = await response.Content.ReadFromJsonAsync(JsonOptions); Assert.That(body!.SkippedCardCount, Is.EqualTo(1), "Unknown 99999999 must be skipped and counted."); using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var stored = await db.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card) .FirstAsync(v => v.Id == body.ViewerId); Assert.That(stored.Cards.Count, Is.EqualTo(1), "Only the known card should be stored."); var owned = stored.Cards.Single(); Assert.That(owned.Card.Id, Is.EqualTo(10001001L)); Assert.That(owned.Count, Is.EqualTo(2)); Assert.That(owned.IsProtected, Is.True); } [Test] public async Task ImportViewer_clamps_card_count_to_max_copies() { using var factory = new SVSimTestFactory(); using var client = factory.CreateClient(); var response = await client.PostAsJsonAsync("/admin/import_viewer", new ImportViewerRequest { SteamId = 76_561_198_111_222_334UL, OwnedCards = new List { new() { CardId = 10001002L, Count = 5 } } }); var body = await response.Content.ReadFromJsonAsync(JsonOptions); using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var stored = await db.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card) .FirstAsync(v => v.Id == body!.ViewerId); Assert.That(stored.Cards.Single().Count, Is.EqualTo(3), "Count must clamp to OwnedCardEntry.MaxCopies (3)."); } [Test] public async Task ImportViewer_replaces_existing_card_collection() { using var factory = new SVSimTestFactory(); const ulong steamId = 76_561_198_111_222_335UL; long viewerId = await factory.SeedViewerAsync(steamId: steamId); await factory.SeedOwnedCardAsync(viewerId, 10001001L, count: 3); using var client = factory.CreateClient(); var response = await client.PostAsJsonAsync("/admin/import_viewer", new ImportViewerRequest { SteamId = steamId, OwnedCards = new List { new() { CardId = 10001002L, Count = 1 } } }); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), await response.Content.ReadAsStringAsync()); using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var stored = await db.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card) .FirstAsync(v => v.Id == viewerId); Assert.That(stored.Cards.Select(c => c.Card.Id), Is.EquivalentTo(new[] { 10001002L }), "Full replace: the pre-seeded 10001001 must be gone, only 10001002 present."); } }