383 lines
15 KiB
C#
383 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
|
|
};
|
|
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"));
|
|
}
|
|
} |