namespace RpgRoller.Tests; public sealed class ServiceSkillRollTests { [Fact] public void CharacterSkillAndRollOperations_CheckAuthorizationAndValidationBranches() { using var harness = ServiceTestSupport.CreateHarness(3, 4, 5, 6); var service = harness.Service; service.Register("gm", "Password123", "GM"); service.Register("owner", "Password123", "Owner"); service.Register("other", "Password123", "Other"); var gmSession = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken; var ownerSession = ServiceTestSupport.GetValue(service.Login("owner", "Password123")).SessionToken; var otherSession = ServiceTestSupport.GetValue(service.Login("other", "Password123")).SessionToken; var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Main", "dnd5e")); var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Owner Char", campaign.Id)); var noPermissionUpdate = service.UpdateCharacter(otherSession, character.Id, "Renamed", campaign.Id); Assert.False(noPermissionUpdate.Succeeded); var invalidCharacterName = service.UpdateCharacter(ownerSession, character.Id, "", campaign.Id); Assert.False(invalidCharacterName.Succeeded); var missingTargetCampaign = service.UpdateCharacter(ownerSession, character.Id, "Renamed", Guid.NewGuid()); Assert.False(missingTargetCampaign.Succeeded); var noSkillName = service.CreateSkill(ownerSession, character.Id, "", "1d20", 0, false); Assert.False(noSkillName.Succeeded); var invalidExpression = service.CreateSkill(ownerSession, character.Id, "Skill", "5D+4", 0, false); Assert.False(invalidExpression.Succeeded); var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Skill", "1d20+2", 0, false)); var missingSkillUpdate = service.UpdateSkill(ownerSession, Guid.NewGuid(), "X", "1d20", 0, false); Assert.False(missingSkillUpdate.Succeeded); var forbiddenSkillUpdate = service.UpdateSkill(otherSession, skill.Id, "X", "1d20", 0, false); Assert.False(forbiddenSkillUpdate.Succeeded); var gmSkillUpdate = service.UpdateSkill(gmSession, skill.Id, "GM Edit", "2d6+1", 0, false); Assert.True(gmSkillUpdate.Succeeded); var missingRoll = service.RollSkill(ownerSession, Guid.NewGuid(), "public"); Assert.False(missingRoll.Succeeded); var invalidVisibility = service.RollSkill(ownerSession, skill.Id, "hidden"); Assert.False(invalidVisibility.Succeeded); var forbiddenRoll = service.RollSkill(otherSession, skill.Id, "public"); Assert.False(forbiddenRoll.Succeeded); var privateRoll = service.RollSkill(ownerSession, skill.Id, "private"); var publicRoll = service.RollSkill(ownerSession, skill.Id, "public"); Assert.True(privateRoll.Succeeded); Assert.True(publicRoll.Succeeded); var ownerLog = service.GetCampaignLog(ownerSession, campaign.Id); var gmLog = service.GetCampaignLog(gmSession, campaign.Id); var outsiderLog = service.GetCampaignLog(otherSession, campaign.Id); Assert.Equal(2, ServiceTestSupport.GetValue(ownerLog).Count); Assert.Equal(2, ServiceTestSupport.GetValue(gmLog).Count); Assert.False(outsiderLog.Succeeded); Assert.All(ServiceTestSupport.GetValue(ownerLog), entry => Assert.NotEmpty(entry.Dice)); Assert.All(ServiceTestSupport.GetValue(gmLog), entry => Assert.NotEmpty(entry.Dice)); var state = service.GetCampaignStateSnapshot(ownerSession, campaign.Id); var missingState = service.GetCampaignStateSnapshot(ownerSession, Guid.NewGuid()); Assert.True(state.Succeeded); Assert.False(missingState.Succeeded); Assert.True(ServiceTestSupport.GetValue(state).LogVersion > 1); } [Fact] public void CampaignLogPage_ReturnsInitialWindowAndIncrementalAppend() { using var harness = ServiceTestSupport.CreateHarness(6, 5, 4, 3, 2, 6, 5, 4, 3, 2, 6, 5); var service = harness.Service; service.Register("gm-page", "Password123", "GM"); service.Register("owner-page", "Password123", "Owner"); var gmSession = ServiceTestSupport.GetValue(service.Login("gm-page", "Password123")).SessionToken; var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-page", "Password123")).SessionToken; var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Paged", "d6")); var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Hero", campaign.Id)); var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Stealth", "2D+1", 1, true)); var rollIds = new List(); for (var i = 0; i < 5; i++) rollIds.Add(ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public")).RollId); var initialPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, limit: 3)); Assert.Equal(3, initialPage.Entries.Length); Assert.Equal(rollIds[2], initialPage.Entries[0].RollId); Assert.Equal(rollIds[^1], initialPage.Entries[^1].RollId); Assert.Equal(rollIds[^1], initialPage.Cursor); Assert.True(initialPage.HasMore); Assert.False(initialPage.ResetRequired); Assert.All(initialPage.Entries, entry => { Assert.False(string.IsNullOrWhiteSpace(entry.SummaryText)); Assert.False(string.IsNullOrWhiteSpace(entry.RollerLabel)); }); var latestRoll = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public")); var incrementalPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, initialPage.Cursor, 3)); Assert.Single(incrementalPage.Entries); Assert.Equal(latestRoll.RollId, incrementalPage.Entries[0].RollId); Assert.Equal(latestRoll.RollId, incrementalPage.Cursor); Assert.False(incrementalPage.HasMore); Assert.False(incrementalPage.ResetRequired); } [Fact] public void CampaignLogPage_RequestsResetWhenIncrementalGapExceedsLimit() { using var harness = ServiceTestSupport.CreateHarness(6, 5, 4, 3, 2, 6, 5, 4, 3, 2, 6, 5); var service = harness.Service; service.Register("gm-gap", "Password123", "GM"); service.Register("owner-gap", "Password123", "Owner"); var gmSession = ServiceTestSupport.GetValue(service.Login("gm-gap", "Password123")).SessionToken; var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-gap", "Password123")).SessionToken; var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Gap", "d6")); var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Hero", campaign.Id)); var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Stealth", "2D+1", 1, true)); var rollIds = new List(); for (var i = 0; i < 6; i++) rollIds.Add(ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public")).RollId); var gapPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, rollIds[0], 3)); Assert.True(gapPage.ResetRequired); Assert.True(gapPage.HasMore); Assert.Equal(3, gapPage.Entries.Length); Assert.Equal(rollIds[3], gapPage.Entries[0].RollId); Assert.Equal(rollIds[^1], gapPage.Entries[^1].RollId); Assert.Equal(rollIds[^1], gapPage.Cursor); } [Fact] public void CampaignLogPage_BuildsD6AndDndSpecialEventBadges() { using var harness = ServiceTestSupport.CreateHarness(6, 4, 6, 6, 2, 20, 1); var service = harness.Service; service.Register("gm-special", "Password123", "GM"); service.Register("owner-special", "Password123", "Owner"); var gmSession = ServiceTestSupport.GetValue(service.Login("gm-special", "Password123")).SessionToken; var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-special", "Password123")).SessionToken; var d6Campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "D6 Special", "d6")); var d6Character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Wild Hero", d6Campaign.Id)); var d6Skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, d6Character.Id, "Stealth", "2D+1", 1, true)); _ = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, d6Skill.Id, "public")); var d6Entry = Assert.Single(ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, d6Campaign.Id, limit: 5)).Entries); var d6Badges = Assert.IsType(d6Entry.EventBadges); Assert.Equal("w6", Assert.Single(d6Badges)); Assert.Equal("6, 4, 6, 6, 2", d6Entry.SummaryText); var dndCampaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Dnd Special", "dnd5e")); var dndCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Natural Hero", dndCampaign.Id)); var dndSkill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, dndCharacter.Id, "Attack", "1d20+5", 0, false)); _ = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, dndSkill.Id, "public")); _ = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, dndSkill.Id, "public")); var dndEntries = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, dndCampaign.Id, limit: 5)).Entries; var firstDndBadges = Assert.IsType(dndEntries[0].EventBadges); Assert.Equal("n20", Assert.Single(firstDndBadges)); Assert.Equal("20", dndEntries[0].SummaryText); var secondDndBadges = Assert.IsType(dndEntries[1].EventBadges); Assert.Equal("n1", Assert.Single(secondDndBadges)); Assert.Equal("1", dndEntries[1].SummaryText); } [Fact] public void RollDetail_ReturnsVisibleDetailAndHidesPrivateRoll() { using var harness = ServiceTestSupport.CreateHarness(6, 5, 4, 3, 2, 6); var service = harness.Service; service.Register("gm-detail", "Password123", "GM"); service.Register("owner-detail", "Password123", "Owner"); service.Register("observer-detail", "Password123", "Observer"); service.Register("outsider-detail", "Password123", "Outsider"); var gmSession = ServiceTestSupport.GetValue(service.Login("gm-detail", "Password123")).SessionToken; var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-detail", "Password123")).SessionToken; var observerSession = ServiceTestSupport.GetValue(service.Login("observer-detail", "Password123")).SessionToken; var outsiderSession = ServiceTestSupport.GetValue(service.Login("outsider-detail", "Password123")).SessionToken; var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Detail", "d6")); var ownerCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Hero", campaign.Id)); _ = ServiceTestSupport.GetValue(service.CreateCharacter(observerSession, "Watcher", campaign.Id)); var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, "Stealth", "2D+1", 1, true)); var privateRoll = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "private")); var publicRoll = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public")); var gmDetail = ServiceTestSupport.GetValue(service.GetRollDetail(gmSession, privateRoll.RollId)); var ownerDetail = ServiceTestSupport.GetValue(service.GetRollDetail(ownerSession, privateRoll.RollId)); var observerPublicDetail = ServiceTestSupport.GetValue(service.GetRollDetail(observerSession, publicRoll.RollId)); var observerPrivateDetail = service.GetRollDetail(observerSession, privateRoll.RollId); var outsiderPublicDetail = service.GetRollDetail(outsiderSession, publicRoll.RollId); Assert.NotEmpty(gmDetail.Dice); Assert.Equal(privateRoll.RollId, gmDetail.RollId); Assert.Equal(privateRoll.Breakdown, ownerDetail.Breakdown); Assert.Equal(publicRoll.RollId, observerPublicDetail.RollId); Assert.False(observerPrivateDetail.Succeeded); Assert.Equal("roll_not_found", observerPrivateDetail.Error!.Code); Assert.False(outsiderPublicDetail.Succeeded); Assert.Equal("roll_not_found", outsiderPublicDetail.Error!.Code); } [Fact] public void CustomRoll_UsesCampaignRuleset_AndAppearsAsCustomRollInLog() { using var harness = ServiceTestSupport.CreateHarness(20); var service = harness.Service; service.Register("gm-custom", "Password123", "GM"); service.Register("owner-custom", "Password123", "Owner"); service.Register("other-custom", "Password123", "Other"); var gmSession = ServiceTestSupport.GetValue(service.Login("gm-custom", "Password123")).SessionToken; var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-custom", "Password123")).SessionToken; var otherSession = ServiceTestSupport.GetValue(service.Login("other-custom", "Password123")).SessionToken; var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Custom", "dnd5e")); var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Hero", campaign.Id)); var invalidExpression = service.RollCustom(ownerSession, character.Id, "bad", "public"); Assert.False(invalidExpression.Succeeded); Assert.Equal("invalid_expression", invalidExpression.Error!.Code); var forbiddenRoll = service.RollCustom(otherSession, character.Id, "1d20+5", "public"); Assert.False(forbiddenRoll.Succeeded); Assert.Equal("forbidden", forbiddenRoll.Error!.Code); var customRoll = ServiceTestSupport.GetValue(service.RollCustom(ownerSession, character.Id, "1d20+5", "private")); Assert.Equal(Guid.Empty, customRoll.SkillId); Assert.StartsWith("1d20+5 => ", customRoll.Breakdown, StringComparison.Ordinal); var logPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, limit: 5)); var entry = Assert.Single(logPage.Entries); Assert.Equal("Custom roll", entry.SkillName); Assert.Equal("Private (GM view)", entry.VisibilityLabel); Assert.Contains("n20", Assert.IsType(entry.EventBadges)); var log = ServiceTestSupport.GetValue(service.GetCampaignLog(ownerSession, campaign.Id)); Assert.Equal("Custom roll", Assert.Single(log).SkillName); } }