Radicade Public Code

This commit is contained in:
JogmansonReal
2026-10-16 23:33:13 -07:00
commit 55175acdf6
832 changed files with 363261 additions and 0 deletions
+305
View File
@@ -0,0 +1,305 @@
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());
}
}