namespace GmRelay.Bot.Tests.Web; public sealed class PortfolioServiceSourceTests { [Fact] public async Task PortfolioService_ShouldExposePortfolioTablesAndPublicationGuards() { var source = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/Portfolio/PortfolioService.cs"); Assert.Contains("portfolio_games", source, StringComparison.Ordinal); Assert.Contains("portfolio_game_sessions", source, StringComparison.Ordinal); Assert.Contains("portfolio_game_masters", source, StringComparison.Ordinal); Assert.Contains("portfolio_game_reviews", source, StringComparison.Ordinal); Assert.Contains("moderation_status = 'Approved'", source, StringComparison.Ordinal); Assert.Contains("publication_consent_at IS NOT NULL", source, StringComparison.Ordinal); Assert.Contains("s.scheduled_at < now()", source, StringComparison.Ordinal); Assert.Contains("FOR UPDATE", source, StringComparison.Ordinal); Assert.Contains("ON CONFLICT (portfolio_game_id, author_player_id) DO NOTHING", source, StringComparison.Ordinal); } [Fact] public async Task PublicMasterPortfolioQuery_ShouldNotRequirePublicSchedule() { var source = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/Portfolio/PortfolioService.cs"); var publicMasterQuery = PublicMasterQuerySection(source); Assert.Contains("portfolio_game_masters", publicMasterQuery, StringComparison.Ordinal); Assert.DoesNotContain("public_schedule_enabled = true", publicMasterQuery, StringComparison.Ordinal); } [Fact] public async Task PublicClubPortfolioQuery_ShouldRequirePublicSchedule() { var source = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/Portfolio/PortfolioService.cs"); var publicClubQuery = PublicClubQuerySection(source); Assert.Contains("g.public_schedule_enabled = true", publicClubQuery, StringComparison.Ordinal); } [Fact] public async Task ShowcaseSessionQuery_ShouldKeepFourHourFutureWindow() { var source = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/SessionService.cs"); var showcaseQuery = ShowcaseQuerySection(source); Assert.Contains("s.scheduled_at > now() - interval '4 hours'", showcaseQuery, StringComparison.Ordinal); } private static string PublicMasterQuerySection(string source) { var start = source.IndexOf("GetPublicPortfolioGamesForMasterAsync", StringComparison.Ordinal); if (start < 0) return string.Empty; var end = source.IndexOf("GetPublicPortfolioGamesForClubAsync", start, StringComparison.Ordinal); return end < 0 ? source[start..] : source[start..end]; } private static string PublicClubQuerySection(string source) { var start = source.IndexOf("GetPublicPortfolioGamesForClubAsync", StringComparison.Ordinal); if (start < 0) return string.Empty; var end = source.IndexOf("GetPublicPortfolioGameBySlugAsync", start, StringComparison.Ordinal); return end < 0 ? source[start..] : source[start..end]; } private static string ShowcaseQuerySection(string source) { var start = source.IndexOf("GetShowcaseSessionsAsync", StringComparison.Ordinal); if (start < 0) return string.Empty; var end = source.IndexOf("GetShowcaseSessionAsync", start, StringComparison.Ordinal); return end < 0 ? source[start..] : source[start..end]; } private static async Task 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}'."); } }