mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 07:29:00 -07:00
Radicade Public Code
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user