Files
GmRelayBot/tests/GmRelay.Bot.Tests/Web/MasterProfilesTests.cs
T
Toutsu b52d4000b4
PR Checks / test-and-build (pull_request) Successful in 11m56s
fix(web): restore public game pages
Use the existing group_managers.created_at column when picking owner profile links for public pages.

Bump version -> 3.5.1
2026-05-29 09:27:01 +03:00

201 lines
10 KiB
C#

namespace GmRelay.Bot.Tests.Web;
public sealed class MasterProfilesTests
{
[Fact]
public async Task MigrationV028_ShouldAddMasterProfilesWithoutExternalIdentifiers()
{
var migration = await ReadRepositoryFileAsync("src/GmRelay.Bot/Migrations/V028__add_master_profiles.sql");
Assert.Contains("CREATE TABLE master_profiles", migration, StringComparison.Ordinal);
Assert.Contains("player_id", migration, StringComparison.Ordinal);
Assert.Contains("public_slug", migration, StringComparison.Ordinal);
Assert.Contains("is_public", migration, StringComparison.Ordinal);
Assert.Contains("display_name", migration, StringComparison.Ordinal);
Assert.Contains("bio", migration, StringComparison.Ordinal);
Assert.Contains("ux_master_profiles_public_slug", migration, StringComparison.Ordinal);
Assert.DoesNotContain("external_user_id", migration, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("telegram_id", migration, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("discord", migration, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task SessionStore_ShouldExposeSanitizedMasterProfileContracts()
{
var sessionStore = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/ISessionStore.cs");
Assert.Contains("MasterProfileSettings", sessionStore, StringComparison.Ordinal);
Assert.Contains("PublicMasterProfile", sessionStore, StringComparison.Ordinal);
Assert.Contains("PublicMasterClub", sessionStore, StringComparison.Ordinal);
Assert.Contains("GetMasterProfileSettingsAsync", sessionStore, StringComparison.Ordinal);
Assert.Contains("UpdateMasterProfileSettingsAsync", sessionStore, StringComparison.Ordinal);
Assert.Contains("GetPublicMasterProfileBySlugAsync", sessionStore, StringComparison.Ordinal);
var publicProfileSection = RecordSection(sessionStore, "PublicMasterProfile");
Assert.DoesNotContain("AvatarUrl", publicProfileSection, StringComparison.Ordinal);
Assert.DoesNotContain("ExternalUserId", publicProfileSection, StringComparison.Ordinal);
Assert.DoesNotContain("TelegramId", publicProfileSection, StringComparison.Ordinal);
Assert.DoesNotContain("DiscordId", publicProfileSection, StringComparison.Ordinal);
}
[Fact]
public async Task PublicMasterProfilePage_ShouldBePublicAndHideTechnicalIdentityData()
{
var publicProfilePage = await ReadRepositoryFileAsync("src/GmRelay.Web/Components/Pages/PublicMasterProfile.razor");
Assert.Contains("@page \"/gm/{Slug}\"", publicProfilePage, StringComparison.Ordinal);
Assert.Contains("@layout PublicLayout", publicProfilePage, StringComparison.Ordinal);
Assert.Contains("GetPublicMasterProfileBySlugAsync", publicProfilePage, StringComparison.Ordinal);
Assert.DoesNotContain("@attribute [Authorize]", publicProfilePage, StringComparison.Ordinal);
Assert.DoesNotContain("ExternalUserId", publicProfilePage, StringComparison.Ordinal);
Assert.DoesNotContain("TelegramId", publicProfilePage, StringComparison.Ordinal);
Assert.DoesNotContain("DiscordId", publicProfilePage, StringComparison.Ordinal);
Assert.DoesNotContain("AvatarUrl", publicProfilePage, StringComparison.Ordinal);
Assert.DoesNotContain("LinkedIdentity", publicProfilePage, StringComparison.Ordinal);
}
[Fact]
public async Task ProfilePage_ShouldManageMasterProfilePublication()
{
var profilePage = await ReadRepositoryFileAsync("src/GmRelay.Web/Components/Pages/Profile.razor");
var authorizedService = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/AuthorizedSessionService.cs");
Assert.Contains("GetMasterProfileSettingsForCurrentUserAsync", profilePage, StringComparison.Ordinal);
Assert.Contains("UpdateMasterProfileSettingsForCurrentUserAsync", profilePage, StringComparison.Ordinal);
Assert.Contains("masterProfileModel", profilePage, StringComparison.Ordinal);
Assert.Contains("PublicMasterProfileUrl", profilePage, StringComparison.Ordinal);
Assert.Contains("NormalizeMasterProfileSlug", authorizedService, StringComparison.Ordinal);
}
[Fact]
public async Task TelegramLoginEndpoints_ShouldUpsertPlayersForProfileManagement()
{
var program = await ReadRepositoryFileAsync("src/GmRelay.Web/Program.cs");
Assert.Contains("ISessionStore sessionStore", EndpointSection(program, "auth/telegram-webapp"), StringComparison.Ordinal);
Assert.Contains("sessionStore.UpsertPlayerAsync", EndpointSection(program, "auth/telegram-webapp"), StringComparison.Ordinal);
Assert.Contains("ISessionStore sessionStore", EndpointSection(program, "auth/telegram-login"), StringComparison.Ordinal);
Assert.Contains("sessionStore.UpsertPlayerAsync", EndpointSection(program, "auth/telegram-login"), StringComparison.Ordinal);
}
[Fact]
public async Task PublicGamePages_ShouldLinkPublishedMasterProfilesWithoutPrivateIds()
{
var publicClubPage = await ReadRepositoryFileAsync("src/GmRelay.Web/Components/Pages/PublicClub.razor");
var publicSessionPage = await ReadRepositoryFileAsync("src/GmRelay.Web/Components/Pages/PublicSession.razor");
var showcasePage = await ReadRepositoryFileAsync("src/GmRelay.Web/Components/Pages/Showcase.razor");
var sessionService = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/SessionService.cs");
Assert.Contains("MasterProfileSlug", publicClubPage, StringComparison.Ordinal);
Assert.Contains("MasterProfileSlug", publicSessionPage, StringComparison.Ordinal);
Assert.Contains("MasterProfileSlug", showcasePage, StringComparison.Ordinal);
Assert.Contains("master_profiles", sessionService, StringComparison.Ordinal);
Assert.DoesNotContain("targetExternalUserId", PublicQuerySection(sessionService), StringComparison.Ordinal);
Assert.DoesNotContain("target_platform", PublicQuerySection(sessionService), StringComparison.Ordinal);
}
[Fact]
public async Task PublicMasterProfileQueries_ShouldIncludeCoGmManagedPublishedGames()
{
var sessionService = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/SessionService.cs");
var clubsQuery = MethodSection(sessionService, "GetPublicClubsForMasterAsync");
var sessionsQuery = MethodSection(sessionService, "GetPublicSessionsForMasterAsync");
Assert.Contains("JOIN group_managers gm", clubsQuery, StringComparison.Ordinal);
Assert.Contains("JOIN group_managers gm", sessionsQuery, StringComparison.Ordinal);
Assert.DoesNotContain("gm.role = @OwnerRole", clubsQuery, StringComparison.Ordinal);
Assert.DoesNotContain("gm.role = @OwnerRole", sessionsQuery, StringComparison.Ordinal);
}
[Fact]
public async Task PublicOwnerProfileLinks_ShouldOrderByExistingGroupManagerTimestamp()
{
var migration = await ReadRepositoryFileAsync("src/GmRelay.Bot/Migrations/V008__add_group_managers.sql");
var sessionService = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/SessionService.cs");
Assert.Contains("created_at", migration, StringComparison.Ordinal);
Assert.DoesNotContain("added_at", migration, StringComparison.Ordinal);
Assert.DoesNotContain("gm.added_at", PublicQuerySection(sessionService), StringComparison.Ordinal);
Assert.Contains("gm.created_at", PublicQuerySection(sessionService), StringComparison.Ordinal);
}
private static string RecordSection(string source, string recordName)
{
var start = source.IndexOf($"record {recordName}", StringComparison.Ordinal);
if (start < 0)
return string.Empty;
var end = source.IndexOf(");", start, StringComparison.Ordinal);
return end < 0 ? source[start..] : source[start..(end + 2)];
}
private static string PublicQuerySection(string source)
{
var start = source.IndexOf("GetPublicMasterProfileBySlugAsync", StringComparison.Ordinal);
if (start < 0)
return string.Empty;
var end = source.IndexOf("// --- Identity linking", start, StringComparison.Ordinal);
return end < 0 ? source[start..] : source[start..end];
}
private static string MethodSection(string source, string methodName)
{
var start = -1;
var searchFrom = 0;
while (searchFrom < source.Length)
{
var candidate = source.IndexOf(methodName, searchFrom, StringComparison.Ordinal);
if (candidate < 0)
return string.Empty;
var lineStart = source.LastIndexOf('\n', candidate);
var headerStart = lineStart < 0 ? 0 : lineStart + 1;
var header = source[headerStart..candidate];
if (header.Contains("private static async Task", StringComparison.Ordinal))
{
start = candidate;
break;
}
searchFrom = candidate + methodName.Length;
}
var nextMethod = source.IndexOf("\n private static async Task", start + methodName.Length, StringComparison.Ordinal);
if (nextMethod < 0)
{
nextMethod = source.IndexOf("\n public async Task", start + methodName.Length, StringComparison.Ordinal);
}
return nextMethod < 0 ? source[start..] : source[start..nextMethod];
}
private static string EndpointSection(string source, string route)
{
var start = source.IndexOf($"\"/{route}\"", StringComparison.Ordinal);
if (start < 0)
return string.Empty;
var nextEndpoint = source.IndexOf("app.Map", start + route.Length, StringComparison.Ordinal);
return nextEndpoint < 0 ? source[start..] : source[start..nextEndpoint];
}
private static async Task<string> ReadRepositoryFileAsync(string relativePath)
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
var candidate = Path.Combine(directory.FullName, relativePath);
if (File.Exists(candidate))
{
return await File.ReadAllTextAsync(candidate);
}
directory = directory.Parent;
}
throw new FileNotFoundException($"Could not locate repository file '{relativePath}'.");
}
}