mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
306 lines
17 KiB
C#
306 lines
17 KiB
C#
using System.Security.Claims;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using RadicadeServer.API.Progression.Challenges.V2;
|
|
using RadicadeServer.API.Storefront;
|
|
using RadicadeServer.DB;
|
|
using RadicadeServer.RecNet.Accounts;
|
|
using RadicadeServer.RecNet.Avatar;
|
|
using RadicadeServer.Utils;
|
|
using Xunit;
|
|
|
|
namespace RadicadeServer.Tests;
|
|
|
|
public class WeeklyChallengeTests
|
|
{
|
|
[Fact]
|
|
public async Task AdminLibrarySchedulesNextWeekAndPlayerEndpointUsesSavedLineup()
|
|
{
|
|
await using var db = Db();
|
|
var oldKey = DBConfig.AdminApiKey;
|
|
var oldLogs = DBConfig.PersistentLogs;
|
|
var oldEquipment = DBConfig.DefaultEquipmentItems;
|
|
var oldAvatars = DBConfig.DefaultAvatarCatalog;
|
|
DBConfig.DefaultEquipmentItems = JsonSerializer.Serialize(new[] { new Equipment { PrefabName = "Disc", ModificationGuid = "test-skin", FriendlyName = "Test skin" } });
|
|
DBConfig.DefaultAvatarCatalog = "[]";
|
|
var folder = Path.Combine(Path.GetTempPath(), "radicade-weekly-tests-" + Guid.NewGuid().ToString("N"));
|
|
DBConfig.AdminApiKey = "weekly-test-key";
|
|
DBConfig.PersistentLogs = folder;
|
|
try
|
|
{
|
|
var api = new RadicadeServer.Admin.AdminPanelAPI(db);
|
|
var now = DateTime.UtcNow;
|
|
var current = FixtureMap(now);
|
|
var next = FixtureMap(current.EndAt);
|
|
var ids = new List<long>();
|
|
foreach (var definition in current.Challenges.Take(3))
|
|
{
|
|
var payload = JsonSerializer.SerializeToElement(new { name = "Admin " + definition.Name,
|
|
description = definition.Description, tooltip = definition.Tooltip, config = definition.Config });
|
|
var result = Assert.IsType<OkObjectResult>(await api.Insert("challenges", "weekly-test-key", payload));
|
|
ids.Add(Assert.IsType<JsonObject>(result.Value)["id"]!.GetValue<long>());
|
|
}
|
|
var request = new RadicadeServer.Admin.WeeklyScheduleRequest { ChallengeMapId = next.ChallengeMapId, ChallengeIds = ids, RewardKey = "equipment:Disc:test-skin" };
|
|
Assert.IsType<UnauthorizedObjectResult>(await api.SaveWeeklySchedule("wrong", request));
|
|
Assert.IsType<OkObjectResult>(await api.SaveWeeklySchedule("weekly-test-key", request));
|
|
Assert.Equal(5, (await WeeklyChallenges.GetAsync(db, 42, now)).Challenges.Count);
|
|
var scheduled = await WeeklyChallenges.GetAsync(db, 42, current.EndAt);
|
|
Assert.Equal(3, scheduled.Challenges.Count);
|
|
Assert.All(scheduled.Challenges, c => Assert.StartsWith("Admin ", c.Name));
|
|
// A library edit doesn't change the already-published snapshot.
|
|
await api.Update("challenges", ids[0], "weekly-test-key", JsonSerializer.SerializeToElement(new { name = "Draft change" }));
|
|
Assert.Equal(scheduled.Challenges[0].Name, (await WeeklyChallenges.GetAsync(db, 42, current.EndAt)).Challenges[0].Name);
|
|
foreach (var id in ids) await Complete(db, (int)id, now: current.EndAt);
|
|
Assert.Single(StorefrontShared.ParseGiftList((await db.Gifts.SingleAsync()).Data));
|
|
var published = await WeeklyChallenges.GetDefinitionAsync(db, current.EndAt);
|
|
Assert.IsType<ConflictObjectResult>(await api.SaveWeeklySchedule("weekly-test-key", request));
|
|
request.ChallengeMapId = current.ChallengeMapId;
|
|
DBConfig.DefaultAvatarCatalog = JsonSerializer.Serialize(new[] { new AvatarItem { AvatarItemDesc = "admin-avatar", FriendlyName = "Admin avatar", AvatarItemType = 0 } });
|
|
request.RewardKey = "avatar:admin-avatar";
|
|
Assert.IsType<OkObjectResult>(await api.SaveWeeklySchedule("weekly-test-key", request));
|
|
Assert.Equal("admin-avatar", (await WeeklyChallenges.GetAsync(db, 42, now)).Gift.AvatarItemDesc);
|
|
foreach (var id in ids) await Complete(db, (int)id, now: now);
|
|
Assert.Contains(StorefrontShared.ParseGiftList((await db.Gifts.SingleAsync()).Data), g => g.AvatarItemDesc == "admin-avatar");
|
|
request.Revision = (await WeeklyChallenges.GetDefinitionAsync(db, now)).Revision;
|
|
request.RewardKey = "avatar:nonexistent";
|
|
Assert.IsType<BadRequestObjectResult>(await api.SaveWeeklySchedule("weekly-test-key", request));
|
|
request.RewardKey = "equipment:Disc:test-skin";
|
|
request.ChallengeMapId = current.ChallengeMapId - 1;
|
|
Assert.IsType<ConflictObjectResult>(await api.SaveWeeklySchedule("weekly-test-key", request));
|
|
request.ChallengeMapId = next.ChallengeMapId;
|
|
request.ChallengeIds = [ids[0], ids[0], ids[1]];
|
|
Assert.IsType<BadRequestObjectResult>(await api.SaveWeeklySchedule("weekly-test-key", request));
|
|
request.ChallengeIds = [ids[0], ids[1], 99999];
|
|
Assert.IsType<BadRequestObjectResult>(await api.SaveWeeklySchedule("weekly-test-key", request));
|
|
}
|
|
finally
|
|
{
|
|
DBConfig.DefaultEquipmentItems = oldEquipment;
|
|
DBConfig.DefaultAvatarCatalog = oldAvatars;
|
|
DBConfig.AdminApiKey = oldKey;
|
|
DBConfig.PersistentLogs = oldLogs;
|
|
if (Directory.Exists(folder)) Directory.Delete(folder, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UnpublishedWeekHasNoContentAndRejectsProgress()
|
|
{
|
|
await using var db = Db();
|
|
db.ServerConfig.RemoveRange(db.ServerConfig); await db.SaveChangesAsync();
|
|
var map = await WeeklyChallenges.GetAsync(db, 42, Now);
|
|
Assert.False(map.Enabled); Assert.Empty(map.Challenges);
|
|
Assert.Empty(map.Gift.EquipmentModificationGuid);
|
|
Assert.Equal("weekly_not_published", await WeeklyChallenges.UpdateAsync(db, 42, map.ChallengeMapId, 1, "{}", true, Now));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CurrentEditsPreserveMatchingProgressAndNeverReaward()
|
|
{
|
|
await using var db = Db();
|
|
await Complete(db, 1);
|
|
var definition = FixtureMap(Now);
|
|
definition.Gift = new Gift { AvatarItemDesc = "test-avatar", FriendlyName = "Chosen avatar", Context = RadicadeServer.RecNet.Enums.Store.GiftContext.All_Weekly_Challenge_Complete };
|
|
var record = await db.ServerConfig.SingleAsync(x => x.Key == WeeklyChallenges.ScheduleKey(definition.ChallengeMapId));
|
|
record.Value = JsonSerializer.Serialize(definition); await db.SaveChangesAsync();
|
|
Assert.True((await WeeklyChallenges.GetAsync(db, 42, Now)).Challenges[0].Complete);
|
|
await Complete(db, 2); await Complete(db, 3);
|
|
Assert.Equal("test-avatar", Assert.Single(StorefrontShared.ParseGiftList((await db.Gifts.SingleAsync()).Data)).AvatarItemDesc);
|
|
definition.Challenges[0].Config = definition.Challenges[3].Config;
|
|
record = await db.ServerConfig.SingleAsync(x => x.Key == WeeklyChallenges.ScheduleKey(definition.ChallengeMapId));
|
|
record.Value = JsonSerializer.Serialize(definition); await db.SaveChangesAsync();
|
|
var updated = await WeeklyChallenges.GetAsync(db, 42, Now);
|
|
Assert.False(updated.Challenges[0].Complete); Assert.True(updated.CompletedRequired);
|
|
await Complete(db, 1);
|
|
Assert.Single(StorefrontShared.ParseGiftList((await db.Gifts.SingleAsync()).Data));
|
|
definition.Enabled = false;
|
|
record = await db.ServerConfig.SingleAsync(x => x.Key == WeeklyChallenges.ScheduleKey(definition.ChallengeMapId));
|
|
record.Value = JsonSerializer.Serialize(definition); await db.SaveChangesAsync();
|
|
Assert.Empty((await WeeklyChallenges.GetAsync(db, 42, Now)).Challenges);
|
|
Assert.Equal("weekly_not_published", await WeeklyChallenges.UpdateAsync(db, 42, definition.ChallengeMapId, 1, definition.Challenges[0].Config, false, Now));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PendingAvatarGetsConfiguredFallback()
|
|
{
|
|
await using var db = Db();
|
|
var map = FixtureMap(Now);
|
|
map.Gift = new Gift { AvatarItemDesc = "test-avatar", Context = RadicadeServer.RecNet.Enums.Store.GiftContext.All_Weekly_Challenge_Complete };
|
|
map.FallbackTokens = 725;
|
|
(await db.ServerConfig.SingleAsync(x => x.Key == WeeklyChallenges.ScheduleKey(map.ChallengeMapId))).Value = JsonSerializer.Serialize(map);
|
|
await db.SaveChangesAsync();
|
|
await StorefrontShared.PersistGiftBoxAsync(db, 42, new Gift { GiftDropId = 99, AvatarItemDesc = "test-avatar" }, notify: false);
|
|
await Complete(db, 1); await Complete(db, 2); await Complete(db, 3);
|
|
var gifts = StorefrontShared.ParseGiftList((await db.Gifts.SingleAsync()).Data);
|
|
Assert.Equal(2, gifts.Count); Assert.Equal(725, gifts.Single(x => x.GiftDropId != 99).Currency);
|
|
}
|
|
|
|
[Fact]
|
|
public void WeeklyDefinitionValidationRejectsCompletedAndUnrecognizedPredicates()
|
|
{
|
|
foreach (var goal in FixtureMap(Now).Challenges)
|
|
Assert.True(WeeklyChallenges.IsSupportedDefinition(goal.Config));
|
|
Assert.False(WeeklyChallenges.IsSupportedDefinition("{}"));
|
|
Assert.False(WeeklyChallenges.IsSupportedDefinition("{\"ct\":0,\"rc\":[{\"ct\":6,\"vs\":[999]}]}"));
|
|
Assert.False(WeeklyChallenges.IsSupportedDefinition(Completed(FixtureMap(Now).Challenges[0].Config)));
|
|
}
|
|
private static readonly DateTime Now = new(2026, 10, 8, 12, 0, 0, DateTimeKind.Utc);
|
|
private static RecDBContext Db(string? name = null)
|
|
{
|
|
var db = new RecDBContext(TestDbOptions.Create().UseInMemoryDatabase(name ?? Guid.NewGuid().ToString()).Options);
|
|
foreach (var time in new[] { Now, DateTime.UtcNow })
|
|
{
|
|
var map = FixtureMap(time);
|
|
var key = WeeklyChallenges.ScheduleKey(map.ChallengeMapId);
|
|
if (!db.ServerConfig.Any(x => x.Key == key)) db.ServerConfig.Add(new RadicadeServer.RecNet.Config.ServerConfigVal(key, JsonSerializer.Serialize(map)));
|
|
db.SaveChanges();
|
|
}
|
|
return db;
|
|
}
|
|
private static RadicadeServer.RecNet.Progression.Challenges.WeeklyChallengeMap FixtureMap(DateTime now)
|
|
{
|
|
var map = WeeklyChallenges.CreateMap(now);
|
|
map.Enabled = true;
|
|
map.Challenges = new[] { 2, 4, 5, 10, 8 }.Select((e, i) => new RadicadeServer.RecNet.Progression.Challenges.WeeklyChallenge {
|
|
ChallengeId = i + 1, Name = "Goal " + (i + 1), Description = "Test goal", Config = JsonSerializer.Serialize(new { ct = 0, rc = new[] { new { ct = 6, vs = new[] { e } } } }) }).ToList();
|
|
map.Gift = new Gift { EquipmentPrefabName = "Disc", EquipmentModificationGuid = "9d2a3618-12b3-4ca0-be59-ed15d7926d77", Context = RadicadeServer.RecNet.Enums.Store.GiftContext.All_Weekly_Challenge_Complete };
|
|
return map;
|
|
}
|
|
private static string Completed(string config)
|
|
{
|
|
var node = JsonNode.Parse(config)!;
|
|
node["c"] = true;
|
|
node["ipc"] = false;
|
|
node["wc"] = new JsonArray();
|
|
return node.ToJsonString();
|
|
}
|
|
private static async Task Complete(RecDBContext db, int id, long player = 42, DateTime? now = null)
|
|
{
|
|
var time = now ?? Now;
|
|
var map = await WeeklyChallenges.GetAsync(db, player, time);
|
|
Assert.Null(await WeeklyChallenges.UpdateAsync(db, player, map.ChallengeMapId, id,
|
|
Completed(map.Challenges.Single(x => x.ChallengeId == id).Config), true, time));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ThreeOfFivePaysOnceIncludingRetriesAndAllFive()
|
|
{
|
|
await using var db = Db();
|
|
await Complete(db, 1); await Complete(db, 2);
|
|
Assert.Empty(await db.Gifts.ToListAsync());
|
|
await Complete(db, 3); await Complete(db, 3); await Complete(db, 4); await Complete(db, 5);
|
|
var gifts = StorefrontShared.ParseGiftList((await db.Gifts.SingleAsync()).Data);
|
|
Assert.Single(gifts);
|
|
Assert.Equal("9d2a3618-12b3-4ca0-be59-ed15d7926d77", gifts[0].EquipmentModificationGuid);
|
|
Assert.True((await WeeklyChallenges.GetAsync(db, 42, Now)).CompletedRequired);
|
|
Assert.Equal(gifts[0].GiftDropId, (await db.WeeklyChallenges.SingleAsync()).RewardGiftDropId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReconnectPersistsAndPlayersAreIsolated()
|
|
{
|
|
var name = Guid.NewGuid().ToString();
|
|
await using (var db = Db(name)) await Complete(db, 1);
|
|
await using var reopened = Db(name);
|
|
Assert.True((await WeeklyChallenges.GetAsync(reopened, 42, Now)).Challenges[0].Complete);
|
|
Assert.False((await WeeklyChallenges.GetAsync(reopened, 43, Now)).Challenges[0].Complete);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RolloverResetsAndRejectsOldWeek()
|
|
{
|
|
await using var db = Db();
|
|
var before = FixtureMap(Now);
|
|
await Complete(db, 1);
|
|
var next = await WeeklyChallenges.GetAsync(db, 42, before.EndAt);
|
|
Assert.Equal(before.ChallengeMapId + 1, next.ChallengeMapId);
|
|
Assert.All(next.Challenges, x => Assert.False(x.Complete));
|
|
Assert.False(next.Enabled);
|
|
Assert.Empty(next.Challenges);
|
|
Assert.Equal("stale_challenge_map", await WeeklyChallenges.UpdateAsync(db, 42, before.ChallengeMapId, 1,
|
|
Completed(before.Challenges[0].Config), true, before.EndAt));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RejectsAlteredDefinitionsAndInconsistentCompletion()
|
|
{
|
|
await using var db = Db();
|
|
var map = FixtureMap(Now);
|
|
var changed = JsonNode.Parse(Completed(map.Challenges[0].Config))!;
|
|
changed["rc"]![0]!["vs"]![0] = 0;
|
|
Assert.Equal("invalid_challenge_config", await WeeklyChallenges.UpdateAsync(db, 42, map.ChallengeMapId, 1, changed.ToJsonString(), true, Now));
|
|
Assert.Equal("invalid_challenge_config", await WeeklyChallenges.UpdateAsync(db, 42, map.ChallengeMapId, 1, map.Challenges[0].Config, true, Now));
|
|
Assert.Equal("unknown_challenge", await WeeklyChallenges.UpdateAsync(db, 42, map.ChallengeMapId, 100, "{}", true, Now));
|
|
Assert.Empty(await db.WeeklyChallenges.ToListAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OwnedSkinGetsOneTokenBox()
|
|
{
|
|
await using var db = Db();
|
|
var gift = FixtureMap(Now).Gift;
|
|
db.Unlocks.Add(new PlayerUnlocks { AccountId = 42, Equipment = JsonSerializer.Serialize(new[]
|
|
{ new Equipment { PrefabName = gift.EquipmentPrefabName, ModificationGuid = gift.EquipmentModificationGuid } }) });
|
|
await db.SaveChangesAsync();
|
|
await Complete(db, 1); await Complete(db, 2); await Complete(db, 3); await Complete(db, 3);
|
|
var box = Assert.Single(StorefrontShared.ParseGiftList((await db.Gifts.SingleAsync()).Data));
|
|
Assert.Equal(300, box.Currency);
|
|
Assert.True(string.IsNullOrEmpty(box.EquipmentModificationGuid));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConsumedBoxIsNeverRecreatedAndCompletionCannotRegress()
|
|
{
|
|
await using var db = Db();
|
|
await Complete(db, 1); await Complete(db, 2); await Complete(db, 3);
|
|
var row = await db.Gifts.SingleAsync(); row.Data = "[]"; await db.SaveChangesAsync();
|
|
var fresh = FixtureMap(Now);
|
|
Assert.Null(await WeeklyChallenges.UpdateAsync(db, 42, fresh.ChallengeMapId, 1, fresh.Challenges[0].Config, false, Now));
|
|
await Complete(db, 3);
|
|
Assert.Empty(StorefrontShared.ParseGiftList((await db.Gifts.SingleAsync()).Data));
|
|
Assert.True((await WeeklyChallenges.GetAsync(db, 42, Now)).Challenges[0].Complete);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConcurrentCompletionsDoNotLoseProgressOrDuplicateReward()
|
|
{
|
|
var name = Guid.NewGuid().ToString();
|
|
await using (var seed = Db(name)) { }
|
|
await Task.WhenAll(Enumerable.Range(1, 5).Select(async id =>
|
|
{
|
|
await using var db = Db(name); await Complete(db, id);
|
|
}));
|
|
await using var check = Db(name);
|
|
Assert.All((await WeeklyChallenges.GetAsync(check, 42, Now)).Challenges, x => Assert.True(x.Complete));
|
|
Assert.Single(StorefrontShared.ParseGiftList((await check.Gifts.SingleAsync()).Data));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public async Task EndpointAcceptsClientFormAndJson(bool form)
|
|
{
|
|
await using var db = Db();
|
|
var now = DateTime.UtcNow;
|
|
var map = FixtureMap(now);
|
|
var fields = new Dictionary<string, string> { ["ChallengeMapId"] = map.ChallengeMapId.ToString(),
|
|
["ChallengeId"] = "1", ["Complete"] = "True", ["Config"] = Completed(map.Challenges[0].Config) };
|
|
var body = form ? await new FormUrlEncodedContent(fields).ReadAsStringAsync() : JsonSerializer.Serialize(fields);
|
|
var context = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
|
{ new Claim(ClaimTypes.NameIdentifier, "42") }, "test")) };
|
|
context.Request.ContentType = form ? "application/x-www-form-urlencoded" : "application/json";
|
|
context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
|
|
var api = new ChallengeV2(db) { ControllerContext = new ControllerContext { HttpContext = context } };
|
|
Assert.IsType<OkResult>(await api.UpdateProgress());
|
|
var response = Assert.IsType<OkObjectResult>(await api.GetCurrent());
|
|
var wire = JsonSerializer.SerializeToElement(response.Value);
|
|
Assert.Equal(JsonValueKind.String, wire.GetProperty("Challenges")[0].GetProperty("Config").ValueKind);
|
|
Assert.True(wire.GetProperty("Challenges")[0].GetProperty("Complete").GetBoolean());
|
|
Assert.Equal(5, wire.GetProperty("Challenges").GetArrayLength());
|
|
}
|
|
}
|