Persist Rolemaster fumble range
This commit is contained in:
@@ -79,6 +79,35 @@ public sealed class CampaignApiTests : ApiTestBase
|
||||
Assert.Equal("rolemaster", campaign.RulesetId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RolemasterSkillDefinitions_RoundTripFumbleRangeThroughApi()
|
||||
{
|
||||
using var factory = CreateFactory(88, 42, 17);
|
||||
using var gmClient = factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||
|
||||
await RegisterAsync(gmClient, "gm-rm-skill", "Password123", "Game Master");
|
||||
await LoginAsync(gmClient, "gm-rm-skill", "Password123");
|
||||
|
||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Shadow World", "rolemaster"));
|
||||
var character = await PostAsync<CreateCharacterRequest, CharacterSummary>(gmClient, "/api/characters", new("Kalen", campaign.Id));
|
||||
|
||||
var missingFumbleRange = await gmClient.PostAsJsonAsync($"/api/characters/{character.Id}/skills", new CreateSkillRequest("Bad Open Ended", "d100!+35", 0, false));
|
||||
Assert.Equal(HttpStatusCode.BadRequest, missingFumbleRange.StatusCode);
|
||||
|
||||
var group = await PostAsync<CreateSkillGroupRequest, SkillGroupSummary>(gmClient, $"/api/characters/{character.Id}/skill-groups", new("Perception", "d100!+15", 0, false, 5));
|
||||
Assert.Equal(5, group.FumbleRange);
|
||||
|
||||
var skill = await PostAsync<CreateSkillRequest, SkillSummary>(gmClient, $"/api/characters/{character.Id}/skills", new("Awareness", "d100!+35", 0, false, group.Id, 3));
|
||||
Assert.Equal(3, skill.FumbleRange);
|
||||
|
||||
var updatedSkill = await PutAsync<UpdateSkillRequest, SkillSummary>(gmClient, $"/api/skills/{skill.Id}", new("Awareness", "d100!+45", 0, false, group.Id, 4));
|
||||
Assert.Equal(4, updatedSkill.FumbleRange);
|
||||
|
||||
var sheet = await GetAsync<CharacterSheet>(gmClient, $"/api/characters/{character.Id}/sheet");
|
||||
Assert.Equal(5, Assert.Single(sheet.SkillGroups).FumbleRange);
|
||||
Assert.Equal(4, Assert.Single(sheet.Skills).FumbleRange);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkillGroupsAndOwnerTransfer_WorkThroughApi()
|
||||
{
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using RpgRoller.Data;
|
||||
using RpgRoller.Hosting;
|
||||
|
||||
@@ -125,6 +127,16 @@ public sealed class HostingCoverageTests
|
||||
|
||||
Assert.Contains("WildDice", columns);
|
||||
Assert.Contains("AllowFumble", columns);
|
||||
Assert.Contains("FumbleRange", columns);
|
||||
|
||||
using var skillGroupsTableInfoCommand = verifyConnection.CreateCommand();
|
||||
skillGroupsTableInfoCommand.CommandText = "PRAGMA table_info('SkillGroups');";
|
||||
using var skillGroupsTableInfoReader = skillGroupsTableInfoCommand.ExecuteReader();
|
||||
var skillGroupColumns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
while (skillGroupsTableInfoReader.Read())
|
||||
skillGroupColumns.Add(skillGroupsTableInfoReader.GetString(1));
|
||||
|
||||
Assert.Contains("FumbleRange", skillGroupColumns);
|
||||
|
||||
using var rollTableInfoCommand = verifyConnection.CreateCommand();
|
||||
rollTableInfoCommand.CommandText = "PRAGMA table_info('RollLogEntries');";
|
||||
@@ -183,5 +195,130 @@ public sealed class HostingCoverageTests
|
||||
rolesHistoryCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260226160859_AddAuthorizationRolesAndCampaignDeletion';";
|
||||
var rolesHistoryCount = Convert.ToInt32(rolesHistoryCommand.ExecuteScalar());
|
||||
Assert.Equal(1, rolesHistoryCount);
|
||||
|
||||
using var rolemasterHistoryCommand = verifyConnection.CreateCommand();
|
||||
rolemasterHistoryCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260402222501_AddRolemasterFumbleRange';";
|
||||
var rolemasterHistoryCount = Convert.ToInt32(rolemasterHistoryCommand.ExecuteScalar());
|
||||
Assert.Equal(1, rolemasterHistoryCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitializeRpgRollerState_MigratesCopiedDevelopmentDatabaseAndPreservesD6Rolling()
|
||||
{
|
||||
var sourceDbPath = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "RpgRoller", "App_Data", "rpgroller.development.db");
|
||||
var copiedDbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-dev-copy-{Guid.NewGuid():N}.db");
|
||||
File.Copy(Path.GetFullPath(sourceDbPath), copiedDbPath, overwrite: true);
|
||||
|
||||
Guid skillId;
|
||||
Guid ownerUserId;
|
||||
Guid characterId;
|
||||
var campaignCountBefore = 0;
|
||||
var skillCountBefore = 0;
|
||||
using (var connection = new SqliteConnection($"Data Source={copiedDbPath}"))
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
using var countsCommand = connection.CreateCommand();
|
||||
countsCommand.CommandText = """
|
||||
SELECT (SELECT COUNT(*) FROM Campaigns),
|
||||
(SELECT COUNT(*) FROM Skills);
|
||||
""";
|
||||
using var countsReader = countsCommand.ExecuteReader();
|
||||
Assert.True(countsReader.Read());
|
||||
campaignCountBefore = countsReader.GetInt32(0);
|
||||
skillCountBefore = countsReader.GetInt32(1);
|
||||
|
||||
using var existingSkillCommand = connection.CreateCommand();
|
||||
existingSkillCommand.CommandText = """
|
||||
SELECT s.Id, c.OwnerUserId, c.Id
|
||||
FROM Skills s
|
||||
INNER JOIN Characters c ON c.Id = s.CharacterId
|
||||
INNER JOIN Campaigns cp ON cp.Id = c.CampaignId
|
||||
WHERE cp.Ruleset = 'D6'
|
||||
ORDER BY s.Name
|
||||
LIMIT 1;
|
||||
""";
|
||||
using var existingSkillReader = existingSkillCommand.ExecuteReader();
|
||||
Assert.True(existingSkillReader.Read());
|
||||
skillId = Guid.Parse(existingSkillReader.GetString(0));
|
||||
ownerUserId = Guid.Parse(existingSkillReader.GetString(1));
|
||||
characterId = Guid.Parse(existingSkillReader.GetString(2));
|
||||
|
||||
using var sessionCommand = connection.CreateCommand();
|
||||
sessionCommand.CommandText = """
|
||||
INSERT INTO Sessions ("Token", "UserId", "CreatedAtUtc")
|
||||
VALUES ($token, $userId, $createdAtUtc);
|
||||
""";
|
||||
var tokenParameter = sessionCommand.CreateParameter();
|
||||
tokenParameter.ParameterName = "$token";
|
||||
tokenParameter.Value = "migration-test-session";
|
||||
sessionCommand.Parameters.Add(tokenParameter);
|
||||
|
||||
var userParameter = sessionCommand.CreateParameter();
|
||||
userParameter.ParameterName = "$userId";
|
||||
userParameter.Value = ownerUserId.ToString();
|
||||
sessionCommand.Parameters.Add(userParameter);
|
||||
|
||||
var createdAtParameter = sessionCommand.CreateParameter();
|
||||
createdAtParameter.ParameterName = "$createdAtUtc";
|
||||
createdAtParameter.Value = DateTimeOffset.UtcNow.ToString("O");
|
||||
sessionCommand.Parameters.Add(createdAtParameter);
|
||||
|
||||
_ = sessionCommand.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
|
||||
{
|
||||
ContentRootPath = Path.GetTempPath(),
|
||||
EnvironmentName = Environments.Development
|
||||
});
|
||||
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ConnectionStrings:RpgRoller"] = $"Data Source={copiedDbPath}"
|
||||
});
|
||||
builder.Services.AddRpgRollerCore(builder.Configuration, builder.Environment);
|
||||
|
||||
using var app = builder.Build();
|
||||
app.InitializeRpgRollerState();
|
||||
|
||||
using var scope = app.Services.CreateScope();
|
||||
var game = scope.ServiceProvider.GetRequiredService<IGameService>();
|
||||
var rollResult = game.RollSkill("migration-test-session", skillId, "public");
|
||||
Assert.True(rollResult.Succeeded);
|
||||
Assert.NotEmpty(ServiceTestSupport.GetValue(rollResult).Dice);
|
||||
|
||||
var migratedSheet = ServiceTestSupport.GetValue(game.GetCharacterSheet("migration-test-session", characterId));
|
||||
Assert.Contains(migratedSheet.Skills, skill => skill.Id == skillId);
|
||||
|
||||
using var verifyConnection = new SqliteConnection($"Data Source={copiedDbPath}");
|
||||
verifyConnection.Open();
|
||||
|
||||
using var countsAfterCommand = verifyConnection.CreateCommand();
|
||||
countsAfterCommand.CommandText = """
|
||||
SELECT (SELECT COUNT(*) FROM Campaigns),
|
||||
(SELECT COUNT(*) FROM Skills);
|
||||
""";
|
||||
using var countsAfterReader = countsAfterCommand.ExecuteReader();
|
||||
Assert.True(countsAfterReader.Read());
|
||||
Assert.Equal(campaignCountBefore, countsAfterReader.GetInt32(0));
|
||||
Assert.Equal(skillCountBefore, countsAfterReader.GetInt32(1));
|
||||
|
||||
using var skillsTableInfoCommand = verifyConnection.CreateCommand();
|
||||
skillsTableInfoCommand.CommandText = "PRAGMA table_info('Skills');";
|
||||
using var skillsTableInfoReader = skillsTableInfoCommand.ExecuteReader();
|
||||
var skillColumns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
while (skillsTableInfoReader.Read())
|
||||
skillColumns.Add(skillsTableInfoReader.GetString(1));
|
||||
|
||||
Assert.Contains("FumbleRange", skillColumns);
|
||||
|
||||
using var skillGroupsTableInfoCommand = verifyConnection.CreateCommand();
|
||||
skillGroupsTableInfoCommand.CommandText = "PRAGMA table_info('SkillGroups');";
|
||||
using var skillGroupsTableInfoReader = skillGroupsTableInfoCommand.ExecuteReader();
|
||||
var skillGroupColumns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
while (skillGroupsTableInfoReader.Read())
|
||||
skillGroupColumns.Add(skillGroupsTableInfoReader.GetString(1));
|
||||
|
||||
Assert.Contains("FumbleRange", skillGroupColumns);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,4 +92,31 @@ public sealed class ServicePersistenceTests
|
||||
Assert.False(invalidExpressionHarness.Service.RollSkill(ownerSession, skill.Id, "public").Succeeded);
|
||||
Assert.False(service.GetCampaignLog(string.Empty, campaign.Id).Succeeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RolemasterFumbleRange_PersistsAcrossDatabaseReload()
|
||||
{
|
||||
using var harness = ServiceTestSupport.CreateHarness();
|
||||
var service = harness.Service;
|
||||
|
||||
service.Register("gm-rm-persist", "Password123", "GM");
|
||||
service.Register("owner-rm-persist", "Password123", "Owner");
|
||||
|
||||
var gmSession = ServiceTestSupport.GetValue(service.Login("gm-rm-persist", "Password123")).SessionToken;
|
||||
var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-rm-persist", "Password123")).SessionToken;
|
||||
|
||||
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Rolemaster Persistence", "rolemaster"));
|
||||
var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Loremaster", campaign.Id));
|
||||
var group = ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, character.Id, "Perception", "d100!+25", 0, false, 5));
|
||||
var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Read Runes", "d100!+35", 0, false, group.Id, 3));
|
||||
|
||||
using var reloadedHarness = ServiceTestSupport.CreateHarnessFromPath(harness.DbPath);
|
||||
var reloadedSheet = ServiceTestSupport.GetValue(reloadedHarness.Service.GetCharacterSheet(ownerSession, character.Id));
|
||||
|
||||
var reloadedGroup = Assert.Single(reloadedSheet.SkillGroups, current => current.Id == group.Id);
|
||||
Assert.Equal(5, reloadedGroup.FumbleRange);
|
||||
|
||||
var reloadedSkill = Assert.Single(reloadedSheet.Skills, current => current.Id == skill.Id);
|
||||
Assert.Equal(3, reloadedSkill.FumbleRange);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,19 +195,34 @@ public sealed class ServiceSkillGroupAndOwnershipTests
|
||||
var negativeDndSkill = service.CreateSkill(ownerSession, dndCharacter.Id, "Invalid", "1d20-1", 0, false);
|
||||
Assert.False(negativeDndSkill.Succeeded);
|
||||
|
||||
var rolemasterGroup = ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, rolemasterCharacter.Id, "Initiative", "2d10-15", 3, true));
|
||||
Assert.Equal("2d10-15", rolemasterGroup.DiceRollDefinition);
|
||||
var invalidRolemasterOptions = service.CreateSkillGroup(ownerSession, rolemasterCharacter.Id, "Invalid", "2d10-15", 3, true);
|
||||
Assert.False(invalidRolemasterOptions.Succeeded);
|
||||
|
||||
var rolemasterGroup = ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, rolemasterCharacter.Id, "Awareness", "d100!+15", 0, false, 5));
|
||||
Assert.Equal("d100!+15", rolemasterGroup.DiceRollDefinition);
|
||||
Assert.Equal(0, rolemasterGroup.WildDice);
|
||||
Assert.False(rolemasterGroup.AllowFumble);
|
||||
Assert.Equal(5, rolemasterGroup.FumbleRange);
|
||||
|
||||
var percentileSkill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, rolemasterCharacter.Id, "Perception", "1d100-20", 4, true));
|
||||
var percentileWithFumbleRange = service.CreateSkill(ownerSession, rolemasterCharacter.Id, "Bad Percentile", "1d100-20", 0, false, null, 5);
|
||||
Assert.False(percentileWithFumbleRange.Succeeded);
|
||||
|
||||
var percentileSkill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, rolemasterCharacter.Id, "Perception", "1d100-20", 0, false, rolemasterGroup.Id));
|
||||
Assert.Equal("d100-20", percentileSkill.DiceRollDefinition);
|
||||
Assert.Equal(0, percentileSkill.WildDice);
|
||||
Assert.False(percentileSkill.AllowFumble);
|
||||
Assert.Null(percentileSkill.FumbleRange);
|
||||
|
||||
var openEndedSkill = ServiceTestSupport.GetValue(service.UpdateSkill(ownerSession, percentileSkill.Id, "Perception", "1d100!+85", 5, true));
|
||||
var missingOpenEndedFumbleRange = service.UpdateSkill(ownerSession, percentileSkill.Id, "Perception", "1d100!+85", 0, false, rolemasterGroup.Id);
|
||||
Assert.False(missingOpenEndedFumbleRange.Succeeded);
|
||||
|
||||
var invalidOpenEndedFumbleRange = service.UpdateSkill(ownerSession, percentileSkill.Id, "Perception", "1d100!+85", 0, false, rolemasterGroup.Id, 96);
|
||||
Assert.False(invalidOpenEndedFumbleRange.Succeeded);
|
||||
|
||||
var openEndedSkill = ServiceTestSupport.GetValue(service.UpdateSkill(ownerSession, percentileSkill.Id, "Perception", "1d100!+85", 0, false, rolemasterGroup.Id, 5));
|
||||
Assert.Equal("d100!+85", openEndedSkill.DiceRollDefinition);
|
||||
Assert.Equal(0, openEndedSkill.WildDice);
|
||||
Assert.False(openEndedSkill.AllowFumble);
|
||||
Assert.Equal(5, openEndedSkill.FumbleRange);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,11 +87,11 @@ public sealed class WorkspaceQueryServiceTests
|
||||
public ServiceResult<bool> DeleteCharacter(string sessionToken, Guid characterId) => throw new NotSupportedException();
|
||||
public ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId) => throw new NotSupportedException();
|
||||
public ServiceResult<IReadOnlyList<CharacterSummary>> GetOwnCharacters(string sessionToken) => throw new NotSupportedException();
|
||||
public ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble) => throw new NotSupportedException();
|
||||
public ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble) => throw new NotSupportedException();
|
||||
public ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null) => throw new NotSupportedException();
|
||||
public ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null) => throw new NotSupportedException();
|
||||
public ServiceResult<bool> DeleteSkillGroup(string sessionToken, Guid skillGroupId) => throw new NotSupportedException();
|
||||
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null) => throw new NotSupportedException();
|
||||
public ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null) => throw new NotSupportedException();
|
||||
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null) => throw new NotSupportedException();
|
||||
public ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null) => throw new NotSupportedException();
|
||||
public ServiceResult<bool> DeleteSkill(string sessionToken, Guid skillId) => throw new NotSupportedException();
|
||||
public ServiceResult<CharacterSheet> GetCharacterSheet(string sessionToken, Guid characterId) => throw new NotSupportedException();
|
||||
public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility) => throw new NotSupportedException();
|
||||
|
||||
Reference in New Issue
Block a user