Puzzles
This commit is contained in:
290
SVSim.UnitTests/Controllers/PuzzleControllerTests.cs
Normal file
290
SVSim.UnitTests/Controllers/PuzzleControllerTests.cs
Normal file
@@ -0,0 +1,290 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Repositories.Viewer;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Controllers;
|
||||
|
||||
public class PuzzleControllerTests
|
||||
{
|
||||
private const string BaseRequestJson =
|
||||
"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""";
|
||||
|
||||
[Test]
|
||||
public async Task Info_returns_25_groups_with_puzzles()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var response = await client.PostAsync("/basic_puzzle/info",
|
||||
new StringContent(BaseRequestJson, Encoding.UTF8, "application/json"));
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
// Controllers return the inner data payload; the wrapping {data_headers, data} envelope
|
||||
// is added by ShadowverseTranslationMiddleware which the test factory bypasses, so the
|
||||
// root element here IS the array (see PracticeControllerTests for the same pattern).
|
||||
var data = doc.RootElement;
|
||||
Assert.That(data.ValueKind, Is.EqualTo(JsonValueKind.Array));
|
||||
Assert.That(data.GetArrayLength(), Is.EqualTo(25));
|
||||
|
||||
var g301 = data.EnumerateArray().Single(g => g.GetProperty("puzzle_master_id").GetString() == "301");
|
||||
Assert.That(g301.GetProperty("is_all_cleared").GetBoolean(), Is.False);
|
||||
Assert.That(g301.GetProperty("puzzle_data").GetArrayLength(), Is.EqualTo(3));
|
||||
// String-on-wire assertion: puzzle_master_id ships as a JSON string, not number.
|
||||
Assert.That(g301.GetProperty("puzzle_master_id").ValueKind, Is.EqualTo(JsonValueKind.String));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Info_reflects_per_viewer_clears()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
// Resolve the repo from a scope, not factory.Services directly (scoped service constraint).
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var clearRepo = scope.ServiceProvider.GetRequiredService<IPuzzleClearRepository>();
|
||||
await clearRepo.UpsertClearAsync(viewerId, 37, 0);
|
||||
}
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var body = await (await client.PostAsync("/basic_puzzle/info",
|
||||
new StringContent(BaseRequestJson, Encoding.UTF8, "application/json")))
|
||||
.Content.ReadAsStringAsync();
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var g301 = doc.RootElement.EnumerateArray()
|
||||
.Single(g => g.GetProperty("puzzle_master_id").GetString() == "301");
|
||||
var p37 = g301.GetProperty("puzzle_data").EnumerateArray()
|
||||
.Single(p => p.GetProperty("puzzle_id").GetString() == "37");
|
||||
Assert.That(p37.GetProperty("is_cleared").GetBoolean(), Is.True);
|
||||
|
||||
Assert.That(g301.GetProperty("is_mission_target").GetBoolean(), Is.True,
|
||||
"Round 1 mission still incomplete (1/3) so group 301 is still a mission target");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OpenPuzzleDialog_returns_one_group()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var req = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","puzzle_master_id":301}""";
|
||||
var body = await (await client.PostAsync("/basic_puzzle/open_puzzle_dialog",
|
||||
new StringContent(req, Encoding.UTF8, "application/json"))).Content.ReadAsStringAsync();
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var root = doc.RootElement;
|
||||
Assert.That(root.GetProperty("puzzle_quest").GetArrayLength(), Is.EqualTo(3));
|
||||
Assert.That(root.GetProperty("puzzle_quest_chara_id").GetString(), Is.EqualTo("3704"));
|
||||
Assert.That(root.GetProperty("is_display_badge").GetBoolean(), Is.False);
|
||||
Assert.That(root.GetProperty("is_display_puzzle_new").GetBoolean(), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OpenPuzzleDialog_unknown_group_returns_empty_payload()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var req = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","puzzle_master_id":99999}""";
|
||||
var resp = await client.PostAsync("/basic_puzzle/open_puzzle_dialog",
|
||||
new StringContent(req, Encoding.UTF8, "application/json"));
|
||||
|
||||
Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||
var root = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement;
|
||||
Assert.That(root.GetProperty("puzzle_quest").GetArrayLength(), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Start_returns_empty_array()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var req = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","puzzle_id":1}""";
|
||||
var body = await (await client.PostAsync("/basic_puzzle/start",
|
||||
new StringContent(req, Encoding.UTF8, "application/json"))).Content.ReadAsStringAsync();
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
Assert.That(doc.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Array));
|
||||
Assert.That(doc.RootElement.GetArrayLength(), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Mission_returns_19_entries_ordered_and_progress_tracked()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
// Clear 2 of 3 puzzles in group 301 (the Round-1 mission target).
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var clearRepo = scope.ServiceProvider.GetRequiredService<IPuzzleClearRepository>();
|
||||
await clearRepo.UpsertClearAsync(viewerId, 37, 0);
|
||||
await clearRepo.UpsertClearAsync(viewerId, 38, 0);
|
||||
}
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var body = await (await client.PostAsync("/basic_puzzle/mission",
|
||||
new StringContent(BaseRequestJson, Encoding.UTF8, "application/json"))).Content.ReadAsStringAsync();
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var data = doc.RootElement;
|
||||
Assert.That(data.GetArrayLength(), Is.EqualTo(19));
|
||||
|
||||
var round1 = data.EnumerateArray()
|
||||
.Single(m => m.GetProperty("mission_name").GetString() == "Clear all Round 1 puzzles");
|
||||
Assert.That(round1.GetProperty("total_count").GetString(), Is.EqualTo("2"));
|
||||
Assert.That(round1.GetProperty("require_number").GetString(), Is.EqualTo("3"));
|
||||
Assert.That(round1.GetProperty("is_achieved").GetBoolean(), Is.False);
|
||||
|
||||
var special = data.EnumerateArray()
|
||||
.Single(m => m.GetProperty("mission_name").GetString() == "Clear all Special Round puzzles");
|
||||
Assert.That(special.GetProperty("total_count").GetString(), Is.EqualTo("0"),
|
||||
"Special-Round missions always surface as 0 in Phase 1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Finish_loss_is_stateless_and_returns_loss_shape()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var req = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","puzzle_id":37,"retry_count":0,"is_win":false}""";
|
||||
var body = await (await client.PostAsync("/basic_puzzle/finish",
|
||||
new StringContent(req, Encoding.UTF8, "application/json"))).Content.ReadAsStringAsync();
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var data = doc.RootElement;
|
||||
|
||||
// Loss-specific: win_count is the NUMBER 0, not the string "1".
|
||||
Assert.That(data.GetProperty("win_count").ValueKind, Is.EqualTo(JsonValueKind.Number));
|
||||
Assert.That(data.GetProperty("win_count").GetInt32(), Is.EqualTo(0));
|
||||
Assert.That(data.GetProperty("achieved_info").GetProperty("mission_start_data").GetArrayLength(), Is.EqualTo(0));
|
||||
Assert.That(data.GetProperty("reward_list").GetArrayLength(), Is.EqualTo(0));
|
||||
|
||||
// No DB writes.
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
Assert.That(await ctx.ViewerPuzzleClears.CountAsync(), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Finish_win_persists_clear_and_returns_win_shape()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var req = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","puzzle_id":37,"retry_count":0,"is_win":true}""";
|
||||
var body = await (await client.PostAsync("/basic_puzzle/finish",
|
||||
new StringContent(req, Encoding.UTF8, "application/json"))).Content.ReadAsStringAsync();
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var data = doc.RootElement;
|
||||
|
||||
// Win-specific: win_count is the STRING "1".
|
||||
Assert.That(data.GetProperty("win_count").ValueKind, Is.EqualTo(JsonValueKind.String));
|
||||
Assert.That(data.GetProperty("win_count").GetString(), Is.EqualTo("1"));
|
||||
|
||||
// 1/3 in group 301 → no mission completion yet.
|
||||
Assert.That(data.GetProperty("achieved_info").GetProperty("achieved_mission_list").GetArrayLength(), Is.EqualTo(0));
|
||||
Assert.That(data.GetProperty("reward_list").GetArrayLength(), Is.EqualTo(0));
|
||||
|
||||
// mission_start_data still contains the un-achieved Round-1 mission.
|
||||
var starts = data.GetProperty("achieved_info").GetProperty("mission_start_data");
|
||||
Assert.That(starts.EnumerateArray().Any(e => e.GetProperty("mission_name").GetString() == "Clear all Round 1 puzzles"), Is.True);
|
||||
|
||||
// Clear was persisted.
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
Assert.That(await ctx.ViewerPuzzleClears.AnyAsync(c => c.ViewerId == viewerId && c.PuzzleId == 37), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Finish_completes_mission_grants_reward_and_toggles_mission_target()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
|
||||
// The Round-1 mission rewards LeaderSkin 3704. SeedGlobalsAsync's leaderskins.csv may
|
||||
// already include this id; insert defensively (skip if exists) so the test is
|
||||
// independent of seed data shape.
|
||||
using (var setup = factory.Services.CreateScope())
|
||||
{
|
||||
var ctx = setup.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
if (!await ctx.LeaderSkins.AnyAsync(s => s.Id == 3704))
|
||||
{
|
||||
ctx.LeaderSkins.Add(new LeaderSkinEntry { Id = 3704, Name = "Round1Reward" });
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var clearRepo = setup.ServiceProvider.GetRequiredService<IPuzzleClearRepository>();
|
||||
await clearRepo.UpsertClearAsync(viewerId, 37, 0);
|
||||
await clearRepo.UpsertClearAsync(viewerId, 38, 0);
|
||||
}
|
||||
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
var req = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","puzzle_id":39,"retry_count":0,"is_win":true}""";
|
||||
var body = await (await client.PostAsync("/basic_puzzle/finish",
|
||||
new StringContent(req, Encoding.UTF8, "application/json"))).Content.ReadAsStringAsync();
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var data = doc.RootElement;
|
||||
var ai = data.GetProperty("achieved_info");
|
||||
|
||||
// Achievement banner emitted.
|
||||
Assert.That(ai.GetProperty("achieved_mission_list").GetArrayLength(), Is.EqualTo(1));
|
||||
Assert.That(ai.GetProperty("achieved_mission_list")[0].GetProperty("achieved_message").GetString(),
|
||||
Is.EqualTo("Cleared all Round 1 puzzles"));
|
||||
|
||||
// mission_reward_* prefixed shape (NOT reward_detail_id/number).
|
||||
var mrl = ai.GetProperty("achieved_mission_reward_list");
|
||||
Assert.That(mrl.GetArrayLength(), Is.EqualTo(1));
|
||||
Assert.That(mrl[0].GetProperty("mission_reward_type").GetString(), Is.EqualTo("10"));
|
||||
Assert.That(mrl[0].GetProperty("mission_reward_detail_id").GetString(), Is.EqualTo("3704"));
|
||||
|
||||
// Top-level reward_list mirrors as TreasureReward shape (reward_id / reward_num).
|
||||
var rl = data.GetProperty("reward_list");
|
||||
Assert.That(rl.GetArrayLength(), Is.EqualTo(1));
|
||||
Assert.That(rl[0].GetProperty("reward_id").GetString(), Is.EqualTo("3704"));
|
||||
Assert.That(rl[0].GetProperty("reward_num").GetString(), Is.EqualTo("1"));
|
||||
|
||||
// Viewer collection updated — owns the leader skin now.
|
||||
using var verify = factory.Services.CreateScope();
|
||||
var verifyCtx = verify.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var viewer = await verifyCtx.Viewers.Include(v => v.LeaderSkins).FirstAsync(v => v.Id == viewerId);
|
||||
Assert.That(viewer.LeaderSkins.Any(s => s.Id == 3704), Is.True);
|
||||
|
||||
// mission_start_data no longer contains the achieved Round-1 mission.
|
||||
var starts = ai.GetProperty("mission_start_data");
|
||||
Assert.That(starts.EnumerateArray().Any(e => e.GetProperty("mission_name").GetString() == "Clear all Round 1 puzzles"),
|
||||
Is.False);
|
||||
|
||||
// puzzle_list entry for group 301 has is_mission_target=false now.
|
||||
var g301 = data.GetProperty("puzzle_list").EnumerateArray()
|
||||
.Single(g => g.GetProperty("puzzle_master_id").GetString() == "301");
|
||||
Assert.That(g301.GetProperty("is_all_cleared").GetBoolean(), Is.True);
|
||||
Assert.That(g301.GetProperty("is_mission_target").GetBoolean(), Is.False);
|
||||
}
|
||||
}
|
||||
64
SVSim.UnitTests/Importers/GlobalsImporterPuzzleTests.cs
Normal file
64
SVSim.UnitTests/Importers/GlobalsImporterPuzzleTests.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Database;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Importers;
|
||||
|
||||
public class GlobalsImporterPuzzleTests
|
||||
{
|
||||
[Test]
|
||||
public async Task ImportsAllPuzzleGroupsAndPuzzles()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync();
|
||||
|
||||
var ctx = factory.Services.GetRequiredService<SVSimDbContext>();
|
||||
Assert.That(await ctx.PuzzleGroups.CountAsync(), Is.EqualTo(25),
|
||||
"25 groups in the captured /basic_puzzle/info (puzzle_master_ids 1..9 plus 301..316)");
|
||||
Assert.That(await ctx.Puzzles.CountAsync(), Is.GreaterThan(100),
|
||||
"~110 puzzles total across all groups");
|
||||
|
||||
// Spot-check group 301 (the Round-1 character group, contains puzzles 37/38/39).
|
||||
var g301 = await ctx.PuzzleGroups.Include(g => g.Puzzles).FirstAsync(g => g.Id == 301);
|
||||
Assert.That(g301.BasicTitleTextId, Is.EqualTo("Puzzle_QuestSelect_0301"));
|
||||
Assert.That(g301.PuzzleCharaId, Is.EqualTo(3704));
|
||||
Assert.That(g301.Puzzles.Select(p => p.Id).OrderBy(x => x), Is.EqualTo(new[] { 37, 38, 39 }));
|
||||
Assert.That(g301.DifficultyNameListJson, Does.Contain("\"Beginner\":\"0\""));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IsIdempotent()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync();
|
||||
await factory.SeedGlobalsAsync(); // second run — must not duplicate
|
||||
|
||||
var ctx = factory.Services.GetRequiredService<SVSimDbContext>();
|
||||
Assert.That(await ctx.PuzzleGroups.CountAsync(), Is.EqualTo(25));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ImportsAllPuzzleMissionsWithRoundMapping()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync();
|
||||
|
||||
var ctx = factory.Services.GetRequiredService<SVSimDbContext>();
|
||||
Assert.That(await ctx.PuzzleMissions.CountAsync(), Is.EqualTo(19),
|
||||
"19 entries in the captured /basic_puzzle/mission");
|
||||
|
||||
// "Clear all Round 1 puzzles" -> target group 301 + AchievedMessage derived.
|
||||
var round1 = await ctx.PuzzleMissions.FirstAsync(m => m.MissionName == "Clear all Round 1 puzzles");
|
||||
Assert.That(round1.TargetPuzzleGroupId, Is.EqualTo(301));
|
||||
Assert.That(round1.AchievedMessage, Is.EqualTo("Cleared all Round 1 puzzles"));
|
||||
Assert.That(round1.RequireNumber, Is.EqualTo(3));
|
||||
Assert.That(round1.RewardType, Is.EqualTo(10)); // LeaderSkin
|
||||
Assert.That(round1.RewardDetailId, Is.EqualTo(3704L)); // chara_id matching group 301
|
||||
Assert.That(round1.RewardNumber, Is.EqualTo(1));
|
||||
|
||||
// Special-Round mission -> TargetPuzzleGroupId is null (deferred per Phase 1).
|
||||
var special = await ctx.PuzzleMissions.FirstAsync(m => m.MissionName == "Clear all Special Round puzzles");
|
||||
Assert.That(special.TargetPuzzleGroupId, Is.Null);
|
||||
}
|
||||
}
|
||||
62
SVSim.UnitTests/Repositories/PuzzleCatalogRepositoryTests.cs
Normal file
62
SVSim.UnitTests/Repositories/PuzzleCatalogRepositoryTests.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Repositories.Globals;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Repositories;
|
||||
|
||||
public class PuzzleCatalogRepositoryTests
|
||||
{
|
||||
[Test]
|
||||
public async Task GetAllGroupsWithPuzzles_returns_25_groups_each_with_puzzles()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync();
|
||||
var repo = factory.Services.GetRequiredService<IPuzzleCatalogRepository>();
|
||||
|
||||
var groups = await repo.GetAllGroupsWithPuzzles();
|
||||
|
||||
Assert.That(groups, Has.Count.EqualTo(25));
|
||||
Assert.That(groups.All(g => g.Puzzles.Count > 0), Is.True,
|
||||
"every group must have its Puzzles navigation populated");
|
||||
var g301 = groups.Single(g => g.Id == 301);
|
||||
Assert.That(g301.Puzzles.Select(p => p.Id).OrderBy(x => x), Is.EqualTo(new[] { 37, 38, 39 }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetGroupWithPuzzles_returns_one_group_or_null()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync();
|
||||
var repo = factory.Services.GetRequiredService<IPuzzleCatalogRepository>();
|
||||
|
||||
var g = await repo.GetGroupWithPuzzles(301);
|
||||
Assert.That(g, Is.Not.Null);
|
||||
Assert.That(g!.Puzzles, Has.Count.EqualTo(3));
|
||||
|
||||
var missing = await repo.GetGroupWithPuzzles(99999);
|
||||
Assert.That(missing, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAllMissionsOrdered_returns_19_missions_in_correct_order()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync();
|
||||
var repo = factory.Services.GetRequiredService<IPuzzleCatalogRepository>();
|
||||
|
||||
var missions = await repo.GetAllMissionsOrdered();
|
||||
Assert.That(missions, Has.Count.EqualTo(19));
|
||||
|
||||
// Captured order: by OrderId asc, then CampaignCommenceTime desc.
|
||||
var pairs = missions.Select(m => (m.OrderId, m.CampaignCommenceTime)).ToList();
|
||||
for (int i = 1; i < pairs.Count; i++)
|
||||
{
|
||||
var prev = pairs[i - 1]; var cur = pairs[i];
|
||||
Assert.That(prev.OrderId, Is.LessThanOrEqualTo(cur.OrderId));
|
||||
if (prev.OrderId == cur.OrderId)
|
||||
Assert.That(prev.CampaignCommenceTime, Is.GreaterThanOrEqualTo(cur.CampaignCommenceTime));
|
||||
}
|
||||
}
|
||||
}
|
||||
50
SVSim.UnitTests/Repositories/PuzzleClearRepositoryTests.cs
Normal file
50
SVSim.UnitTests/Repositories/PuzzleClearRepositoryTests.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Repositories.Viewer;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Repositories;
|
||||
|
||||
public class PuzzleClearRepositoryTests
|
||||
{
|
||||
[Test]
|
||||
public async Task UpsertClear_inserts_then_updates_idempotently()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
var repo = factory.Services.GetRequiredService<IPuzzleClearRepository>();
|
||||
|
||||
var clearsBefore = await repo.GetClearedPuzzleIds(viewerId);
|
||||
Assert.That(clearsBefore, Is.Empty);
|
||||
|
||||
await repo.UpsertClearAsync(viewerId, puzzleId: 37, retryCount: 2);
|
||||
await repo.UpsertClearAsync(viewerId, puzzleId: 37, retryCount: 0); // better clear; BestRetryCount should drop to 0
|
||||
await repo.UpsertClearAsync(viewerId, puzzleId: 38, retryCount: 1);
|
||||
|
||||
var ids = await repo.GetClearedPuzzleIds(viewerId);
|
||||
Assert.That(ids, Is.EquivalentTo(new[] { 37, 38 }));
|
||||
|
||||
var ctx = factory.Services.GetRequiredService<SVSimDbContext>();
|
||||
var row37 = await ctx.ViewerPuzzleClears.FirstAsync(c => c.ViewerId == viewerId && c.PuzzleId == 37);
|
||||
Assert.That(row37.BestRetryCount, Is.EqualTo(0), "BestRetryCount is min across all wins");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetClearedPuzzleIdsByGroup_groups_by_FK()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync(); // need Puzzles table populated for GroupId FKs
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
var repo = factory.Services.GetRequiredService<IPuzzleClearRepository>();
|
||||
|
||||
await repo.UpsertClearAsync(viewerId, 37, 0); // group 301
|
||||
await repo.UpsertClearAsync(viewerId, 38, 0); // group 301
|
||||
await repo.UpsertClearAsync(viewerId, 64, 0); // group 306
|
||||
|
||||
var byGroup = await repo.GetClearedPuzzleIdsByGroup(viewerId);
|
||||
Assert.That(byGroup[301], Is.EquivalentTo(new[] { 37, 38 }));
|
||||
Assert.That(byGroup[306], Is.EquivalentTo(new[] { 64 }));
|
||||
Assert.That(byGroup.Keys, Does.Not.Contain(999));
|
||||
}
|
||||
}
|
||||
53
SVSim.UnitTests/Services/PuzzleMissionEvaluatorTests.cs
Normal file
53
SVSim.UnitTests/Services/PuzzleMissionEvaluatorTests.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.EmulatedEntrypoint.Services;
|
||||
|
||||
namespace SVSim.UnitTests.Services;
|
||||
|
||||
public class PuzzleMissionEvaluatorTests
|
||||
{
|
||||
private static readonly PuzzleMissionEntry Round1 = new()
|
||||
{ Id = 1, MissionName = "Clear all Round 1 puzzles", RequireNumber = 3, TargetPuzzleGroupId = 301 };
|
||||
private static readonly PuzzleMissionEntry SpecialAll = new()
|
||||
{ Id = 2, MissionName = "Clear all Special Round puzzles", RequireNumber = 8, TargetPuzzleGroupId = null };
|
||||
|
||||
private readonly PuzzleMissionEvaluator _e = new();
|
||||
|
||||
[Test]
|
||||
public void Evaluate_unmapped_mission_always_zero()
|
||||
{
|
||||
var cleared = new Dictionary<int, HashSet<int>> { [316] = new() { 106, 107, 108 } };
|
||||
var result = _e.Evaluate(new[] { SpecialAll }, cleared);
|
||||
|
||||
Assert.That(result.Single().TotalCount, Is.EqualTo(0));
|
||||
Assert.That(result.Single().IsAchieved, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Evaluate_mapped_mission_counts_clears_in_target_group_capped()
|
||||
{
|
||||
var partial = new Dictionary<int, HashSet<int>> { [301] = new() { 37, 38 } };
|
||||
Assert.That(_e.Evaluate(new[] { Round1 }, partial).Single().TotalCount, Is.EqualTo(2));
|
||||
Assert.That(_e.Evaluate(new[] { Round1 }, partial).Single().IsAchieved, Is.False);
|
||||
|
||||
var complete = new Dictionary<int, HashSet<int>> { [301] = new() { 37, 38, 39 } };
|
||||
Assert.That(_e.Evaluate(new[] { Round1 }, complete).Single().IsAchieved, Is.True);
|
||||
|
||||
// Imagine a future where the group has more puzzles than RequireNumber — cap at RequireNumber.
|
||||
var over = new Dictionary<int, HashSet<int>> { [301] = new() { 37, 38, 39, 999 } };
|
||||
Assert.That(_e.Evaluate(new[] { Round1 }, over).Single().TotalCount, Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FreshlyCompleted_returns_only_missions_flipping_true()
|
||||
{
|
||||
var before = new Dictionary<int, HashSet<int>> { [301] = new() { 37, 38 } };
|
||||
var after = new Dictionary<int, HashSet<int>> { [301] = new() { 37, 38, 39 } };
|
||||
|
||||
var fresh = _e.FreshlyCompleted(new[] { Round1, SpecialAll }, before, after);
|
||||
Assert.That(fresh, Has.Count.EqualTo(1));
|
||||
Assert.That(fresh[0].Mission.Id, Is.EqualTo(Round1.Id));
|
||||
|
||||
// Re-evaluating with same before==after returns no fresh completions.
|
||||
Assert.That(_e.FreshlyCompleted(new[] { Round1, SpecialAll }, after, after), Is.Empty);
|
||||
}
|
||||
}
|
||||
99
SVSim.UnitTests/Services/RewardGrantServiceTests.cs
Normal file
99
SVSim.UnitTests/Services/RewardGrantServiceTests.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Services;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Services;
|
||||
|
||||
public class RewardGrantServiceTests
|
||||
{
|
||||
[Test]
|
||||
public async Task Sleeve_added_to_viewer_collection()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
// Pick an Id above the seeded sleeves.csv range so this test doesn't collide with the
|
||||
// reference-CSV importer SVSimTestFactory runs at host construction.
|
||||
const int testSleeveId = 2_000_000_000;
|
||||
var sleeve = new SleeveEntry { Id = testSleeveId }; // SleeveEntry has no Name field; Id only
|
||||
ctx.Sleeves.Add(sleeve);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
var viewer = await ctx.Viewers.Include(v => v.Sleeves).FirstAsync(v => v.Id == viewerId);
|
||||
var svc = scope.ServiceProvider.GetRequiredService<RewardGrantService>();
|
||||
|
||||
var entry = svc.Apply(viewer, UserGoodsType.Sleeve, detailId: testSleeveId, num: 1);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Assert.That(viewer.Sleeves.Any(s => s.Id == testSleeveId), Is.True);
|
||||
Assert.That(entry.RewardType, Is.EqualTo((int)UserGoodsType.Sleeve));
|
||||
Assert.That(entry.RewardId, Is.EqualTo((long)testSleeveId));
|
||||
Assert.That(entry.RewardNum, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Rupy_sets_currency_post_state_total()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var viewer = await ctx.Viewers.FirstAsync(v => v.Id == viewerId);
|
||||
viewer.Currency.Rupees = 100UL;
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
var svc = scope.ServiceProvider.GetRequiredService<RewardGrantService>();
|
||||
|
||||
// Reward grants 50; final balance becomes 150 and reward_num on the wire is the new total.
|
||||
var entry = svc.Apply(viewer, UserGoodsType.Rupy, detailId: 0, num: 50);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Assert.That(viewer.Currency.Rupees, Is.EqualTo(150UL));
|
||||
Assert.That(entry.RewardNum, Is.EqualTo(150));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task LeaderSkin_added_idempotently()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
// Pick an Id above the seeded leaderskins.csv range so this test doesn't collide with
|
||||
// the reference-CSV importer SVSimTestFactory runs at host construction.
|
||||
const int testSkinId = 9_999_999;
|
||||
ctx.LeaderSkins.Add(new LeaderSkinEntry { Id = testSkinId, Name = "Round1Reward" });
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
var viewer = await ctx.Viewers.Include(v => v.LeaderSkins).FirstAsync(v => v.Id == viewerId);
|
||||
var svc = scope.ServiceProvider.GetRequiredService<RewardGrantService>();
|
||||
|
||||
svc.Apply(viewer, UserGoodsType.Skin, testSkinId, 1);
|
||||
svc.Apply(viewer, UserGoodsType.Skin, testSkinId, 1); // second grant is a no-op on collection size
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Assert.That(viewer.LeaderSkins.Count(s => s.Id == testSkinId), Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Card_reward_throws_NotSupported()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
var viewer = await ctx.Viewers.FirstAsync(v => v.Id == viewerId);
|
||||
var svc = scope.ServiceProvider.GetRequiredService<RewardGrantService>();
|
||||
|
||||
Assert.Throws<NotSupportedException>(() =>
|
||||
svc.Apply(viewer, UserGoodsType.Card, 10001001L, 1));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user