Delete owned campaigns on user delete and preserve campaign characters

This commit is contained in:
2026-02-26 19:15:59 +01:00
parent 13113f9d40
commit fa7f88e209
3 changed files with 60 additions and 1 deletions

View File

@@ -53,6 +53,7 @@ Gameplay capabilities now include:
- Character owner selection in edit modal backed by existing-username dropdown data - Character owner selection in edit modal backed by existing-username dropdown data
- Role-aware authorization with admin role support (including admin user/role management) - 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) - 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) - 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 - 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 - Campaign management supports character deletion by character owner or admin

View File

@@ -80,4 +80,53 @@ public sealed class ServiceAdminAndCampaignDeletionTests
using var db = harness.CreateDbContext(); using var db = harness.CreateDbContext();
Assert.Empty(db.RollLogEntries.Where(entry => entry.CampaignId == adminDeletedCampaign.Id)); 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);
}
} }

View File

@@ -321,10 +321,19 @@ public sealed class GameService : IGameService
return ServiceResult<bool>.Failure("user_not_found", "User was not found."); return ServiceResult<bool>.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 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) foreach (var campaignId in gmCampaignIds)
DeleteCampaignLocked(campaignId); 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) foreach (var characterId in ownedCharacterIds)
DeleteCharacterLocked(characterId); DeleteCharacterLocked(characterId);