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
@@ -0,0 +1,234 @@
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RadicadeServer;
using RadicadeServer.API.Config;
using RadicadeServer.DB;
using RadicadeServer.RecNet.Accounts.Local;
using RadicadeServer.RecNet.Auth;
using Xunit;
namespace RadicadeServer.Tests;
public class AccountOnboardingSettingsTests
{
// Every key the 2023 title screen reads to decide which page of its own new-user flow to open.
// They all have to be written, not just the ones this server checks itself: a key the client looks
// for and cannot find makes it resume at a page it believes it already showed, and a true one skips
// that page outright. This is the whole reason a brand-new account opens on "Welcome to Rec Room!".
private static readonly string[] AccountCreationKeys =
[
"Recroom.AccountCreation.HasStarted",
"Recroom.AccountCreation.HasChosenUsername",
"Recroom.AccountCreation.HasCreatedPassword",
"Recroom.AccountCreation.HasFinished",
"Recroom.AccountCreation.FinishedAvatarCustomization",
"Recroom.AccountCreation.AcceptedCodeOfConduct",
"Recroom.AccountCreation.HasImportedFriends"
];
[Fact]
public void NewAccountSettings_LeaveOnboardingIncompleteAndRequireOrientation()
{
var settings = JsonSerializer.Deserialize<List<Setting>>(DBConfig.NewAccountSettings2023)!;
string Value(string key) => settings.Single(setting => setting.Key == key).Value;
foreach (var key in AccountCreationKeys)
Assert.Equal("False", Value(key));
Assert.Equal("0", Value("Recroom.OOBE"));
Assert.Equal("1", Value("Radicade.NeedsOrientation"));
Assert.DoesNotContain(settings, setting => setting.Key == "HAS_COMPLETED_ORIENTATION");
}
[Fact]
public void AccountCreationInProgressSettings_ResumeAtThePageAfterWhatThePlayerSubmitted()
{
var settings = JsonSerializer.Deserialize<List<Setting>>(
DBConfig.AccountCreationInProgressSettings2023(usernameChosen: true, passwordCreated: true))!;
string Value(string key) => settings.Single(setting => setting.Key == key).Value;
// An account the flow itself created: it started, the name and password are in, and the pages
// the player has not reached yet (avatar, Code of Conduct, finished) are still open.
Assert.Equal("True", Value("Recroom.AccountCreation.HasStarted"));
Assert.Equal("True", Value("Recroom.AccountCreation.HasChosenUsername"));
Assert.Equal("True", Value("Recroom.AccountCreation.HasCreatedPassword"));
Assert.Equal("False", Value("Recroom.AccountCreation.HasFinished"));
Assert.Equal("False", Value("Recroom.AccountCreation.FinishedAvatarCustomization"));
Assert.Equal("False", Value("Recroom.AccountCreation.AcceptedCodeOfConduct"));
Assert.Equal("1", Value("Radicade.NeedsOrientation"));
// A create that carried neither: the client still has to ask for both.
var bare = JsonSerializer.Deserialize<List<Setting>>(
DBConfig.AccountCreationInProgressSettings2023(usernameChosen: false, passwordCreated: false))!;
Assert.Equal("False", bare.Single(setting => setting.Key == "Recroom.AccountCreation.HasChosenUsername").Value);
Assert.Equal("False", bare.Single(setting => setting.Key == "Recroom.AccountCreation.HasCreatedPassword").Value);
}
[Fact]
public void Regular2023DefaultSettings_DoNotForceNewAccountOnboarding()
{
var settings = JsonSerializer.Deserialize<List<Setting>>(DBConfig.DefaultSettings2023)!;
Assert.DoesNotContain(settings, setting => setting.Key.StartsWith("Recroom.AccountCreation.", StringComparison.Ordinal));
Assert.DoesNotContain(settings, setting => setting.Key == "Radicade.NeedsOrientation");
Assert.Contains(settings, setting => setting.Key == "HAS_COMPLETED_ORIENTATION" && setting.Value == "True");
}
[Fact]
public async Task CannotMarkOnboardingFinishedUntilPasswordBirthdayAndCocAreSaved()
{
await using var db = CreateDb();
db.Profiles.Add(new Profile { accountId = 2, username = "NewUser", displayName = "NewUser", profileImage = "default" });
db.Players.Add(new PlayerData { AccountId = 2, Hashword = "hashed", Birthday = new DateTime(2000, 1, 1) });
db.PlayerSettings.Add(new PlayerSettings
{
AccountId = 2,
Settings = "[{\"Key\":\"Recroom.AccountCreation.HasChosenUsername\",\"Value\":\"True\"}]"
});
await db.SaveChangesAsync();
var api = CreateApi(db);
await api.SetSetting(new Setting { Key = "Recroom.AccountCreation.HasFinished", Value = "True" });
var saved = (await db.PlayerSettings.SingleAsync()).DeserializeSettings();
Assert.DoesNotContain(saved, setting => setting.Key == "Recroom.AccountCreation.HasFinished");
Assert.DoesNotContain(saved, setting => setting.Key == "Radicade.NeedsOrientation");
}
[Fact]
public async Task CompletingOnboardingSetsOrientationMarkerOnlyAfterRequiredFieldsArePresent()
{
await using var db = CreateDb();
db.Profiles.Add(new Profile { accountId = 2, username = "NewUser", displayName = "NewUser", profileImage = "default" });
db.Players.Add(new PlayerData
{
AccountId = 2,
Hashword = "hashed",
Birthday = new DateTime(2000, 1, 1),
CodeOfConductAccepted = true
});
db.PlayerSettings.Add(new PlayerSettings
{
AccountId = 2,
Settings = "[{\"Key\":\"Recroom.AccountCreation.HasChosenUsername\",\"Value\":\"True\"}]"
});
await db.SaveChangesAsync();
var api = CreateApi(db);
await api.SetSetting(new Setting { Key = "Recroom.AccountCreation.HasFinished", Value = "True" });
var saved = (await db.PlayerSettings.SingleAsync()).DeserializeSettings();
Assert.Contains(saved, setting => setting.Key == "Recroom.AccountCreation.HasFinished" && setting.Value == "True");
Assert.Contains(saved, setting => setting.Key == "Radicade.NeedsOrientation" && setting.Value == "1");
}
/// <summary>
/// The flow records the acceptance as a setting on this build's own Code of Conduct page; the
/// account's <c>account/me/acceptcoc</c> route is never called. Before this, an account whose
/// player had walked that page still answered <c>hasAcceptedCodeOfConduct = false</c> - so the
/// client offered the page again on the next boot, and the finish marker was refused over a flag
/// the client's own pages never write.
/// </summary>
[Fact]
public async Task TheConductPageRecordsTheAcceptanceOnTheAccountAndUnblocksTheFinishMarker()
{
await using var db = CreateDb();
db.Profiles.Add(new Profile { accountId = 2, username = "NewUser", displayName = "NewUser", profileImage = "default" });
db.Players.Add(new PlayerData { AccountId = 2, Hashword = "hashed", Birthday = new DateTime(2000, 1, 1) });
db.PlayerSettings.Add(new PlayerSettings
{
AccountId = 2,
Settings = "[{\"Key\":\"Recroom.AccountCreation.HasChosenUsername\",\"Value\":\"True\"}]"
});
await db.SaveChangesAsync();
var api = CreateApi(db);
await api.SetSetting(new Setting { Key = "Recroom.AccountCreation.AcceptedCodeOfConduct", Value = "True" });
Assert.True((await db.Players.AsNoTracking().SingleAsync()).CodeOfConductAccepted);
// And that is now enough for the flow to be able to finish.
await api.SetSetting(new Setting { Key = "Recroom.AccountCreation.HasFinished", Value = "True" });
var saved = (await db.PlayerSettings.SingleAsync()).DeserializeSettings();
Assert.Contains(saved, setting => setting.Key == "Recroom.AccountCreation.HasFinished" && setting.Value == "True");
Assert.Contains(saved, setting => setting.Key == "Radicade.NeedsOrientation" && setting.Value == "1");
}
/// <summary>
/// A birthday that never arrived must not hold the finish marker hostage. Every account the flow
/// has created on this server still reads 0001-01-01 - its age page has never handed this server
/// one - so requiring it meant no player could ever leave the flow, and the title screen kept
/// opening on "Welcome back! - Continue creating account" for an account whose every other page
/// was done.
/// </summary>
[Fact]
public async Task TheFinishMarkerIsRecordedForAnAccountWhoseAgePageNeverArrived()
{
await using var db = CreateDb();
db.Profiles.Add(new Profile { accountId = 2, username = "NewUser", displayName = "NewUser", profileImage = "default" });
db.Players.Add(new PlayerData { AccountId = 2, Hashword = "hashed", Birthday = null });
db.PlayerSettings.Add(new PlayerSettings
{
AccountId = 2,
Settings = "[{\"Key\":\"Recroom.AccountCreation.HasChosenUsername\",\"Value\":\"True\"}]"
});
await db.SaveChangesAsync();
var api = CreateApi(db);
await api.SetSetting(new Setting { Key = "Recroom.AccountCreation.AcceptedCodeOfConduct", Value = "True" });
await api.SetSetting(new Setting { Key = "Recroom.AccountCreation.HasFinished", Value = "True" });
var saved = (await db.PlayerSettings.SingleAsync()).DeserializeSettings();
Assert.Contains(saved, setting => setting.Key == "Recroom.AccountCreation.HasFinished" && setting.Value == "True");
// Still owed, so the account keeps asking for it until the age page hands one over.
Assert.Null((await db.Players.AsNoTracking().SingleAsync()).Birthday);
}
[Theory]
[InlineData("")]
[InlineData("hashed-password")]
public async Task SettingsDoNotSkipAnUnfinishedPasswordPage(string passwordHash)
{
await using var db = CreateDb();
db.Players.Add(new PlayerData { AccountId = 2, Hashword = passwordHash });
db.PlayerSettings.Add(new PlayerSettings
{
AccountId = 2,
Settings = "[{\"Key\":\"Recroom.AccountCreation.HasCreatedPassword\",\"Value\":\"True\"}," +
"{\"Key\":\"Recroom.AccountCreation.HasFinished\",\"Value\":\"True\"}," +
"{\"Key\":\"Recroom.OOBE\",\"Value\":\"100\"}]"
});
await db.SaveChangesAsync();
var settings = CreateApi(db).GetSettings();
var expected = passwordHash.Length == 0 ? "False" : "True";
Assert.Equal(expected, settings.Single(x => x.Key == "Recroom.AccountCreation.HasCreatedPassword").Value);
Assert.Equal(expected, settings.Single(x => x.Key == "Recroom.AccountCreation.HasFinished").Value);
Assert.Equal(passwordHash.Length == 0 ? "0" : "100", settings.Single(x => x.Key == "Recroom.OOBE").Value);
}
private static RecDBContext CreateDb()
{
var options = TestDbOptions.Create()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new RecDBContext(options);
}
private static PlayerSettingsAPI CreateApi(RecDBContext db)
{
var context = new DefaultHttpContext
{
User = new ClaimsPrincipal(new ClaimsIdentity(
[new Claim(ClaimTypes.NameIdentifier, "2")], "test"))
};
return new PlayerSettingsAPI(db)
{
ControllerContext = new ControllerContext { HttpContext = context }
};
}
}