Extract shared game service helpers
This commit is contained in:
387
RpgRoller.Tests/Services/ServiceSharedHelperTests.cs
Normal file
387
RpgRoller.Tests/Services/ServiceSharedHelperTests.cs
Normal file
@@ -0,0 +1,387 @@
|
||||
using RpgRoller.Contracts;
|
||||
using RpgRoller.Domain;
|
||||
using RpgRoller.Services;
|
||||
|
||||
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 Campaign
|
||||
{
|
||||
Id = campaignId,
|
||||
GmUserId = Guid.NewGuid(),
|
||||
Name = "Alpha",
|
||||
Ruleset = RulesetKind.D6,
|
||||
Version = 1
|
||||
};
|
||||
store.CharactersById[characterId] = new Character
|
||||
{
|
||||
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 UserAccount
|
||||
{
|
||||
Id = adminId,
|
||||
Username = "admin",
|
||||
UsernameNormalized = "ADMIN",
|
||||
PasswordHash = "hash",
|
||||
DisplayName = "Admin",
|
||||
Roles = UserRoles.Admin
|
||||
};
|
||||
store.UsersById[gmId] = new UserAccount
|
||||
{
|
||||
Id = gmId,
|
||||
Username = "gm",
|
||||
UsernameNormalized = "GM",
|
||||
PasswordHash = "hash",
|
||||
DisplayName = "GM",
|
||||
Roles = string.Empty
|
||||
};
|
||||
store.UsersById[playerId] = new UserAccount
|
||||
{
|
||||
Id = playerId,
|
||||
Username = "player",
|
||||
UsernameNormalized = "PLAYER",
|
||||
PasswordHash = "hash",
|
||||
DisplayName = "Player",
|
||||
Roles = string.Empty
|
||||
};
|
||||
store.UsersById[outsiderId] = new UserAccount
|
||||
{
|
||||
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 Character
|
||||
{
|
||||
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 UserAccount
|
||||
{
|
||||
Id = userId,
|
||||
Username = "user",
|
||||
UsernameNormalized = "USER",
|
||||
PasswordHash = "hash",
|
||||
DisplayName = "User",
|
||||
Roles = string.Empty
|
||||
};
|
||||
store.UsersById[otherUserId] = new UserAccount
|
||||
{
|
||||
Id = otherUserId,
|
||||
Username = "other",
|
||||
UsernameNormalized = "OTHER",
|
||||
PasswordHash = "hash",
|
||||
DisplayName = "Other",
|
||||
Roles = string.Empty
|
||||
};
|
||||
store.SessionsByToken["valid"] = new UserSession
|
||||
{
|
||||
Token = "valid",
|
||||
UserId = userId,
|
||||
CreatedAtUtc = DateTimeOffset.UtcNow
|
||||
};
|
||||
store.CampaignsById[campaignId] = new Campaign
|
||||
{
|
||||
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 UserAccount
|
||||
{
|
||||
Id = gmId,
|
||||
Username = "gm",
|
||||
UsernameNormalized = "GM",
|
||||
PasswordHash = "hash",
|
||||
DisplayName = "GM",
|
||||
Roles = UserRoles.Admin
|
||||
};
|
||||
store.UsersById[ownerId] = new UserAccount
|
||||
{
|
||||
Id = ownerId,
|
||||
Username = "owner",
|
||||
UsernameNormalized = "OWNER",
|
||||
PasswordHash = "hash",
|
||||
DisplayName = "Owner",
|
||||
Roles = string.Empty
|
||||
};
|
||||
store.UsersById[blankOwnerId] = new UserAccount
|
||||
{
|
||||
Id = blankOwnerId,
|
||||
Username = "blank",
|
||||
UsernameNormalized = "BLANK",
|
||||
PasswordHash = "hash",
|
||||
DisplayName = "",
|
||||
Roles = string.Empty
|
||||
};
|
||||
store.CampaignsById[campaignId] = new Campaign
|
||||
{
|
||||
Id = campaignId,
|
||||
GmUserId = gmId,
|
||||
Name = "Alpha",
|
||||
Ruleset = RulesetKind.Rolemaster,
|
||||
Version = 1
|
||||
};
|
||||
store.CharactersById[characterId] = new Character
|
||||
{
|
||||
Id = characterId,
|
||||
OwnerUserId = ownerId,
|
||||
CampaignId = campaignId,
|
||||
Name = "Scout"
|
||||
};
|
||||
store.SkillGroupsById[skillGroupId] = new SkillGroup
|
||||
{
|
||||
Id = skillGroupId,
|
||||
CharacterId = characterId,
|
||||
Name = "Awareness",
|
||||
DiceRollDefinition = "d100!+15",
|
||||
WildDice = 0,
|
||||
AllowFumble = false,
|
||||
FumbleRange = 5
|
||||
};
|
||||
store.SkillsById[skillId] = new Skill
|
||||
{
|
||||
Id = skillId,
|
||||
CharacterId = characterId,
|
||||
SkillGroupId = skillGroupId,
|
||||
Name = "Perception",
|
||||
DiceRollDefinition = "d100!+25",
|
||||
WildDice = 0,
|
||||
AllowFumble = false,
|
||||
FumbleRange = 3
|
||||
};
|
||||
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.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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user