namespace RpgRoller.Tests; public sealed class ServiceSharedHelperTests { [Fact] public void GameStateStore_TracksCampaignSlicesAndCharacterVersions() { var campaignId = Guid.NewGuid(); var characterId = Guid.NewGuid(); var store = new GameStateStore(); store.CampaignsById[campaignId] = new() { Id = campaignId, GmUserId = Guid.NewGuid(), Name = "Alpha", Ruleset = RulesetKind.D6, Version = 1 }; store.CharactersById[characterId] = new() { Id = characterId, OwnerUserId = Guid.NewGuid(), CampaignId = campaignId, Name = "Scout" }; store.RebuildCampaignStateLocked(); var initialState = store.GetOrCreateCampaignStateLocked(campaignId); Assert.Equal(1, initialState.CharacterVersions[characterId]); store.TouchRosterLocked(campaignId); store.TouchCharacterLocked(campaignId, characterId); store.TouchLogLocked(campaignId); Assert.Equal(4, initialState.TotalVersion); Assert.Equal(2, initialState.RosterVersion); Assert.Equal(2, initialState.LogVersion); Assert.Equal(2, initialState.CharacterVersions[characterId]); store.RemoveCharacterStateLocked(campaignId, characterId); Assert.Empty(initialState.CharacterVersions); store.AddCharacterStateLocked(campaignId, characterId); Assert.Equal(1, initialState.CharacterVersions[characterId]); store.TouchRosterLocked(null); store.TouchCharacterLocked(Guid.NewGuid(), Guid.NewGuid()); store.TouchLogLocked(Guid.NewGuid()); store.RemoveCharacterStateLocked(Guid.NewGuid(), Guid.NewGuid()); } [Fact] public void GameAuthorization_CoversCampaignAndRollVisibility() { var adminId = Guid.NewGuid(); var gmId = Guid.NewGuid(); var playerId = Guid.NewGuid(); var outsiderId = Guid.NewGuid(); var campaignId = Guid.NewGuid(); var store = new GameStateStore(); store.UsersById[adminId] = new() { Id = adminId, Username = "admin", UsernameNormalized = "ADMIN", PasswordHash = "hash", DisplayName = "Admin", Roles = UserRoles.Admin }; store.UsersById[gmId] = new() { Id = gmId, Username = "gm", UsernameNormalized = "GM", PasswordHash = "hash", DisplayName = "GM", Roles = string.Empty }; store.UsersById[playerId] = new() { Id = playerId, Username = "player", UsernameNormalized = "PLAYER", PasswordHash = "hash", DisplayName = "Player", Roles = string.Empty }; store.UsersById[outsiderId] = new() { Id = outsiderId, Username = "outsider", UsernameNormalized = "OUTSIDER", PasswordHash = "hash", DisplayName = "Outsider", Roles = string.Empty }; var campaign = new Campaign { Id = campaignId, GmUserId = gmId, Name = "Alpha", Ruleset = RulesetKind.D6, Version = 1 }; store.CampaignsById[campaignId] = campaign; var playerCharacterId = Guid.NewGuid(); store.CharactersById[playerCharacterId] = new() { Id = playerCharacterId, OwnerUserId = playerId, CampaignId = campaignId, Name = "Scout" }; var publicEntry = new RollLogEntry { Id = Guid.NewGuid(), CampaignId = campaignId, CharacterId = Guid.NewGuid(), SkillId = Guid.NewGuid(), RollerUserId = playerId, Visibility = RollVisibility.Public, Result = 12, Breakdown = "6+6=12", Dice = "[]", TimestampUtc = DateTimeOffset.UtcNow }; var privateEntry = new RollLogEntry { Id = Guid.NewGuid(), CampaignId = publicEntry.CampaignId, CharacterId = publicEntry.CharacterId, SkillId = publicEntry.SkillId, RollerUserId = publicEntry.RollerUserId, Visibility = RollVisibility.Private, Result = publicEntry.Result, Breakdown = publicEntry.Breakdown, Dice = publicEntry.Dice, TimestampUtc = publicEntry.TimestampUtc }; Assert.True(GameAuthorization.HasRole(store.UsersById[adminId], UserRoles.Admin)); Assert.True(GameAuthorization.CanViewCampaign(store, adminId, campaignId)); Assert.True(GameAuthorization.CanViewCampaign(store, gmId, campaignId)); Assert.True(GameAuthorization.CanViewCampaign(store, playerId, campaignId)); Assert.False(GameAuthorization.CanViewCampaign(store, outsiderId, campaignId)); Assert.True(GameAuthorization.CanEditCharacter(playerId, store.CharactersById.Values.Single(), campaign)); Assert.True(GameAuthorization.CanEditCharacter(gmId, store.CharactersById.Values.Single(), campaign)); Assert.False(GameAuthorization.CanEditCharacter(outsiderId, store.CharactersById.Values.Single(), campaign)); Assert.True(GameAuthorization.CanViewRoll(store, gmId, campaign, privateEntry)); Assert.True(GameAuthorization.CanViewRoll(store, playerId, campaign, privateEntry)); Assert.False(GameAuthorization.CanViewRoll(store, outsiderId, campaign, publicEntry)); Assert.False(GameAuthorization.CanViewRoll(store, outsiderId, campaign, privateEntry)); } [Fact] public void GameContextResolver_HandlesUnauthorizedForbiddenAndSuccessPaths() { var userId = Guid.NewGuid(); var otherUserId = Guid.NewGuid(); var campaignId = Guid.NewGuid(); var store = new GameStateStore(); store.UsersById[userId] = new() { Id = userId, Username = "user", UsernameNormalized = "USER", PasswordHash = "hash", DisplayName = "User", Roles = string.Empty }; store.UsersById[otherUserId] = new() { Id = otherUserId, Username = "other", UsernameNormalized = "OTHER", PasswordHash = "hash", DisplayName = "Other", Roles = string.Empty }; store.SessionsByToken["valid"] = new() { Token = "valid", UserId = userId, CreatedAtUtc = DateTimeOffset.UtcNow }; store.CampaignsById[campaignId] = new() { Id = campaignId, GmUserId = otherUserId, Name = "Alpha", Ruleset = RulesetKind.D6, Version = 1 }; var participant = new Character { Id = Guid.NewGuid(), OwnerUserId = userId, CampaignId = campaignId, Name = "Scout" }; store.CharactersById[participant.Id] = participant; Assert.Null(GameContextResolver.ResolveUserLocked(store, string.Empty)); Assert.Null(GameContextResolver.ResolveUserLocked(store, "missing")); Assert.Equal(userId, GameContextResolver.ResolveUserLocked(store, "valid")!.Id); Assert.Equal("unauthorized", GameContextResolver.ResolveCampaignContextLocked(store, string.Empty, campaignId).Error!.Code); Assert.Equal("campaign_not_found", GameContextResolver.ResolveCampaignContextLocked(store, "valid", Guid.NewGuid()).Error!.Code); var forbiddenStore = new GameStateStore(); forbiddenStore.UsersById[userId] = store.UsersById[userId]; forbiddenStore.SessionsByToken["valid"] = store.SessionsByToken["valid"]; forbiddenStore.CampaignsById[campaignId] = store.CampaignsById[campaignId]; Assert.Equal("forbidden", GameContextResolver.ResolveCampaignContextLocked(forbiddenStore, "valid", campaignId).Error!.Code); var context = ServiceTestSupport.GetValue(GameContextResolver.ResolveCampaignContextLocked(store, "valid", campaignId)); Assert.Equal(userId, context.User.Id); Assert.Equal(campaignId, context.Campaign.Id); var orphan = new Character { Id = Guid.NewGuid(), OwnerUserId = userId, CampaignId = null, Name = "Orphan" }; Assert.False(GameContextResolver.TryResolveCharacterCampaignLocked(store, orphan, out _, out var orphanError)); Assert.Equal("character_not_in_campaign", orphanError!.Code); var missingCampaignCharacter = new Character { Id = Guid.NewGuid(), OwnerUserId = orphan.OwnerUserId, CampaignId = Guid.NewGuid(), Name = orphan.Name }; Assert.False(GameContextResolver.TryResolveCharacterCampaignLocked(store, missingCampaignCharacter, out _, out var missingCampaignError)); Assert.Equal("character_not_in_campaign", missingCampaignError!.Code); Assert.True(GameContextResolver.TryResolveCharacterCampaignLocked(store, participant, out var resolvedCampaign, out var noError)); Assert.Equal(campaignId, resolvedCampaign.Id); Assert.Null(noError); } [Fact] public void GameDtoMapper_MapsServiceContractsAndFallbacks() { var gmId = Guid.NewGuid(); var ownerId = Guid.NewGuid(); var blankOwnerId = Guid.NewGuid(); var campaignId = Guid.NewGuid(); var characterId = Guid.NewGuid(); var skillGroupId = Guid.NewGuid(); var skillId = Guid.NewGuid(); var rollId = Guid.NewGuid(); var store = new GameStateStore(); store.UsersById[gmId] = new() { Id = gmId, Username = "gm", UsernameNormalized = "GM", PasswordHash = "hash", DisplayName = "GM", Roles = UserRoles.Admin }; store.UsersById[ownerId] = new() { Id = ownerId, Username = "owner", UsernameNormalized = "OWNER", PasswordHash = "hash", DisplayName = "Owner", Roles = string.Empty }; store.UsersById[blankOwnerId] = new() { Id = blankOwnerId, Username = "blank", UsernameNormalized = "BLANK", PasswordHash = "hash", DisplayName = "", Roles = string.Empty }; store.CampaignsById[campaignId] = new() { Id = campaignId, GmUserId = gmId, Name = "Alpha", Ruleset = RulesetKind.Rolemaster, Version = 1 }; store.CharactersById[characterId] = new() { Id = characterId, OwnerUserId = ownerId, CampaignId = campaignId, Name = "Scout" }; store.SkillGroupsById[skillGroupId] = new() { Id = skillGroupId, CharacterId = characterId, Name = "Awareness", DiceRollDefinition = "d100!+15", WildDice = 0, AllowFumble = false, FumbleRange = 5 }; store.SkillsById[skillId] = new() { Id = skillId, CharacterId = characterId, SkillGroupId = skillGroupId, Name = "Perception", DiceRollDefinition = "d100!+25", WildDice = 0, AllowFumble = false, FumbleRange = 3, RolemasterAutoRetry = true }; store.RebuildCampaignStateLocked(); store.TouchRosterLocked(campaignId); store.TouchCharacterLocked(campaignId, characterId); store.TouchLogLocked(campaignId); var dice = new[] { new RollDieResult(66, false, false, false, false, false, 1, RollDieKinds.RolemasterStandard, 66) }; var logEntry = new RollLogEntry { Id = rollId, CampaignId = campaignId, CharacterId = characterId, SkillId = skillId, RollerUserId = ownerId, Visibility = RollVisibility.Private, Result = 91, Breakdown = "66+25=91", Dice = "[]", TimestampUtc = DateTimeOffset.UtcNow }; var userSummary = GameDtoMapper.ToUserSummary(store.UsersById[gmId]); var adminSummary = GameDtoMapper.ToAdminUserSummary(store.UsersById[gmId]); var campaignOption = GameDtoMapper.ToCampaignOption(store.CampaignsById[campaignId]); var campaignSummary = GameDtoMapper.ToCampaignSummary(store, store.CampaignsById[campaignId]); var campaignRoster = GameDtoMapper.ToCampaignRoster(store, store.CampaignsById[campaignId]); var characterSummary = GameDtoMapper.ToCharacterSummary(store, store.CharactersById[characterId]); var sheet = GameDtoMapper.ToCharacterSheet(store, characterId); var groupSummary = GameDtoMapper.ToSkillGroupSummary(store.SkillGroupsById[skillGroupId]); var skillSummary = GameDtoMapper.ToSkillSummary(store.SkillsById[skillId]); var rollResult = GameDtoMapper.ToRollResult(logEntry, dice); var logDto = GameDtoMapper.ToCampaignLogEntry(logEntry, "Scout", "Perception", "Owner", dice); var logListDto = GameDtoMapper.ToCampaignLogListEntry(logEntry, "Scout", "Perception", "You", "Private (you)", "private-self", "66 | rolemaster", ["r66"]); var detail = GameDtoMapper.ToCampaignRollDetail(logEntry, dice); var snapshot = GameDtoMapper.ToCampaignStateSnapshot(store, campaignId); Assert.Contains(UserRoles.Admin, userSummary.Roles); Assert.Contains(UserRoles.Admin, adminSummary.Roles); Assert.Equal("Alpha", campaignOption.Name); Assert.Equal("rolemaster", campaignSummary.RulesetId); Assert.Single(campaignRoster.Characters); Assert.Equal("Owner", characterSummary.OwnerDisplayName); Assert.Single(sheet.SkillGroups); Assert.Single(sheet.Skills); Assert.Equal(5, groupSummary.FumbleRange); Assert.Equal(3, skillSummary.FumbleRange); Assert.True(skillSummary.RolemasterAutoRetry); Assert.Equal("private", rollResult.Visibility); Assert.Equal("Owner", logDto.RollerDisplayName); Assert.Equal("private-self", logListDto.VisibilityStyle); Assert.Equal(logEntry.Breakdown, detail.Breakdown); Assert.Equal(campaignId, snapshot.CampaignId); Assert.Single(snapshot.CharacterVersions); Assert.Equal("fallback", GameDtoMapper.ResolveOwnerDisplayName(store, blankOwnerId, "fallback")); Assert.Equal("fallback", GameDtoMapper.ResolveOwnerDisplayName(store, Guid.NewGuid(), "fallback")); } }