diff --git a/README.md b/README.md index 6bf37cb..e39a4bd 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Gameplay capabilities now include: - Character owner selection in edit modal backed by existing-username dropdown data - Role-aware authorization with admin role support (including admin user/role management) - Campaign deletion by campaign owner or admin (unlinks characters from the campaign and clears campaign log entries) +- User deletion by admin also deletes campaigns owned by that user and unlinks all characters from those deleted campaigns - Campaign management owner labels use account display names (no GUID fallback rendering) - Character edit flow supports unlinking from campaigns (owner/GM/admin) and assigning to any existing campaign via expanded campaign options - Campaign management supports character deletion by character owner or admin diff --git a/RpgRoller.Tests/Services/ServiceAdminAndCampaignDeletionTests.cs b/RpgRoller.Tests/Services/ServiceAdminAndCampaignDeletionTests.cs index 6819531..8ad314d 100644 --- a/RpgRoller.Tests/Services/ServiceAdminAndCampaignDeletionTests.cs +++ b/RpgRoller.Tests/Services/ServiceAdminAndCampaignDeletionTests.cs @@ -80,4 +80,53 @@ public sealed class ServiceAdminAndCampaignDeletionTests using var db = harness.CreateDbContext(); Assert.Empty(db.RollLogEntries.Where(entry => entry.CampaignId == adminDeletedCampaign.Id)); } + + [Fact] + public void DeleteUser_DeletesOwnedCampaigns_AndUnlinksCharactersInThoseCampaigns() + { + using var harness = ServiceTestSupport.CreateHarness(4, 5, 6); + var service = harness.Service; + + _ = ServiceTestSupport.GetValue(service.Register("admin", "Password123", "Admin")); + _ = ServiceTestSupport.GetValue(service.Register("gm", "Password123", "GM")); + _ = ServiceTestSupport.GetValue(service.Register("player", "Password123", "Player")); + + var adminSession = ServiceTestSupport.GetValue(service.Login("admin", "Password123")).SessionToken; + var gmSession = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken; + var playerSession = ServiceTestSupport.GetValue(service.Login("player", "Password123")).SessionToken; + + var gmOwnedCampaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "GM Campaign", "d6")); + var adminOwnedCampaign = ServiceTestSupport.GetValue(service.CreateCampaign(adminSession, "Admin Campaign", "d6")); + + var gmCharacterInOwnedCampaign = ServiceTestSupport.GetValue(service.CreateCharacter(gmSession, "GM Hero", gmOwnedCampaign.Id)); + var playerCharacterInGmCampaign = ServiceTestSupport.GetValue(service.CreateCharacter(playerSession, "Player Hero", gmOwnedCampaign.Id)); + var gmCharacterOutsideOwnedCampaign = ServiceTestSupport.GetValue(service.CreateCharacter(gmSession, "Visitor", adminOwnedCampaign.Id)); + + var playerSkill = ServiceTestSupport.GetValue(service.CreateSkill(playerSession, playerCharacterInGmCampaign.Id, "Scout", "2D+1", 1, true)); + _ = ServiceTestSupport.GetValue(service.RollSkill(playerSession, playerSkill.Id, "public")); + + var gmUsers = ServiceTestSupport.GetValue(service.GetUsers(adminSession)); + var gmUser = gmUsers.Single(user => string.Equals(user.Username, "gm", StringComparison.OrdinalIgnoreCase)); + + var deleteResult = ServiceTestSupport.GetValue(service.DeleteUser(adminSession, gmUser.Id)); + Assert.True(deleteResult); + + Assert.False(service.GetCampaign(adminSession, gmOwnedCampaign.Id).Succeeded); + + var playerCharacters = ServiceTestSupport.GetValue(service.GetOwnCharacters(playerSession)); + var unlinkedPlayerCharacter = playerCharacters.Single(character => character.Id == playerCharacterInGmCampaign.Id); + Assert.Null(unlinkedPlayerCharacter.CampaignId); + + using var db = harness.CreateDbContext(); + Assert.DoesNotContain(db.Campaigns, campaign => campaign.Id == gmOwnedCampaign.Id); + Assert.Empty(db.RollLogEntries.Where(entry => entry.CampaignId == gmOwnedCampaign.Id)); + + var preservedGmCharacter = db.Characters.Single(character => character.Id == gmCharacterInOwnedCampaign.Id); + Assert.Null(preservedGmCharacter.CampaignId); + + var preservedPlayerCharacter = db.Characters.Single(character => character.Id == playerCharacterInGmCampaign.Id); + Assert.Null(preservedPlayerCharacter.CampaignId); + + Assert.DoesNotContain(db.Characters, character => character.Id == gmCharacterOutsideOwnedCampaign.Id); + } } diff --git a/RpgRoller/Services/GameService.cs b/RpgRoller/Services/GameService.cs index fb81387..dc7141f 100644 --- a/RpgRoller/Services/GameService.cs +++ b/RpgRoller/Services/GameService.cs @@ -321,10 +321,19 @@ public sealed class GameService : IGameService return ServiceResult.Failure("user_not_found", "User was not found."); var gmCampaignIds = m_CampaignsById.Values.Where(campaign => campaign.GmUserId == targetUser.Id).Select(campaign => campaign.Id).ToArray(); + var gmCampaignIdSet = gmCampaignIds.ToHashSet(); + var preservedCharacterIds = m_CharactersById.Values + .Where(character => character.CampaignId.HasValue && gmCampaignIdSet.Contains(character.CampaignId.Value)) + .Select(character => character.Id) + .ToHashSet(); + foreach (var campaignId in gmCampaignIds) DeleteCampaignLocked(campaignId); - var ownedCharacterIds = m_CharactersById.Values.Where(character => character.OwnerUserId == targetUser.Id).Select(character => character.Id).ToArray(); + var ownedCharacterIds = m_CharactersById.Values + .Where(character => character.OwnerUserId == targetUser.Id && !preservedCharacterIds.Contains(character.Id)) + .Select(character => character.Id) + .ToArray(); foreach (var characterId in ownedCharacterIds) DeleteCharacterLocked(characterId);