Files
RpgRoller/RpgRoller.Tests/Services/ServiceSharedHelperTests.cs

385 lines
15 KiB
C#

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"));
}
}