348 lines
18 KiB
C#
348 lines
18 KiB
C#
using Microsoft.Data.Sqlite;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
using Microsoft.EntityFrameworkCore.Migrations;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using RpgRoller.Data;
|
|
using RpgRoller.Hosting;
|
|
|
|
namespace RpgRoller.Tests;
|
|
|
|
public sealed class HostingCoverageTests
|
|
{
|
|
[Fact]
|
|
public void AddRpgRollerCore_WithInMemoryConnectionString_RegistersCoreServices()
|
|
{
|
|
var services = new ServiceCollection();
|
|
var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?> { ["ConnectionStrings:RpgRoller"] = "Data Source=:memory:" }).Build();
|
|
|
|
var environment = new TestWebHostEnvironment { ContentRootPath = Path.GetTempPath() };
|
|
|
|
services.AddRpgRollerCore(configuration, environment);
|
|
|
|
Assert.Contains(services, d => d.ServiceType == typeof(IGameService));
|
|
Assert.Contains(services, d => d.ServiceType == typeof(IDiceRoller));
|
|
}
|
|
|
|
[Fact]
|
|
public void AddRpgRollerCore_WithFileConnectionString_RegistersResolvedSqliteDatabaseFile()
|
|
{
|
|
var services = new ServiceCollection();
|
|
var contentRoot = Path.Combine(Path.GetTempPath(), $"rpgroller-hosting-{Guid.NewGuid():N}");
|
|
var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?> { ["ConnectionStrings:RpgRoller"] = "Data Source=App_Data/rpgroller.db" }).Build();
|
|
var environment = new TestWebHostEnvironment { ContentRootPath = contentRoot };
|
|
|
|
services.AddRpgRollerCore(configuration, environment);
|
|
|
|
using var provider = services.BuildServiceProvider();
|
|
var databaseFile = provider.GetRequiredService<SqliteDatabaseFile>();
|
|
|
|
Assert.Equal(Path.Combine(contentRoot, "App_Data", "rpgroller.db"), databaseFile.Path);
|
|
Assert.True(Directory.Exists(Path.Combine(contentRoot, "App_Data")));
|
|
}
|
|
|
|
[Fact]
|
|
public void SqliteSchemaUpgrader_MigratesLegacySchema()
|
|
{
|
|
var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-legacy-upgrade-{Guid.NewGuid():N}.db");
|
|
var connectionString = $"Data Source={dbPath}";
|
|
|
|
using (var connection = new SqliteConnection(connectionString))
|
|
{
|
|
connection.Open();
|
|
using var command = connection.CreateCommand();
|
|
command.CommandText = """
|
|
CREATE TABLE "Users" (
|
|
"Id" TEXT NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY,
|
|
"Username" TEXT NOT NULL,
|
|
"UsernameNormalized" TEXT NOT NULL,
|
|
"PasswordHash" TEXT NOT NULL,
|
|
"DisplayName" TEXT NOT NULL,
|
|
"ActiveCharacterId" TEXT NULL
|
|
);
|
|
|
|
CREATE TABLE "Sessions" (
|
|
"Token" TEXT NOT NULL CONSTRAINT "PK_Sessions" PRIMARY KEY,
|
|
"UserId" TEXT NOT NULL,
|
|
"CreatedAtUtc" TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE "Campaigns" (
|
|
"Id" TEXT NOT NULL CONSTRAINT "PK_Campaigns" PRIMARY KEY,
|
|
"GmUserId" TEXT NOT NULL,
|
|
"Name" TEXT NOT NULL,
|
|
"Ruleset" TEXT NOT NULL,
|
|
"Version" INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE "Characters" (
|
|
"Id" TEXT NOT NULL CONSTRAINT "PK_Characters" PRIMARY KEY,
|
|
"OwnerUserId" TEXT NOT NULL,
|
|
"CampaignId" TEXT NOT NULL,
|
|
"Name" TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE "Skills" (
|
|
"Id" TEXT NOT NULL CONSTRAINT "PK_Skills" PRIMARY KEY,
|
|
"CharacterId" TEXT NOT NULL,
|
|
"Name" TEXT NOT NULL,
|
|
"DiceRollDefinition" TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE "RollLogEntries" (
|
|
"Id" TEXT NOT NULL CONSTRAINT "PK_RollLogEntries" PRIMARY KEY,
|
|
"CampaignId" TEXT NOT NULL,
|
|
"CharacterId" TEXT NOT NULL,
|
|
"SkillId" TEXT NOT NULL,
|
|
"RollerUserId" TEXT NOT NULL,
|
|
"Visibility" TEXT NOT NULL,
|
|
"Result" INTEGER NOT NULL,
|
|
"Breakdown" TEXT NOT NULL,
|
|
"TimestampUtc" TEXT NOT NULL
|
|
);
|
|
|
|
INSERT INTO "Users" ("Id", "Username", "UsernameNormalized", "PasswordHash", "DisplayName", "ActiveCharacterId")
|
|
VALUES ('00000000-0000-0000-0000-000000000001', 'legacy-admin', 'LEGACY-ADMIN', 'hash', 'Legacy Admin', NULL);
|
|
""";
|
|
_ = command.ExecuteNonQuery();
|
|
}
|
|
|
|
var options = new DbContextOptionsBuilder<RpgRollerDbContext>().UseSqlite(connectionString).Options;
|
|
|
|
using (var db = new RpgRollerDbContext(options))
|
|
{
|
|
SqliteSchemaUpgrader.ApplyPendingChanges(db);
|
|
}
|
|
|
|
using var verifyConnection = new SqliteConnection(connectionString);
|
|
verifyConnection.Open();
|
|
|
|
using var tableInfoCommand = verifyConnection.CreateCommand();
|
|
tableInfoCommand.CommandText = "PRAGMA table_info('Skills');";
|
|
using var tableInfoReader = tableInfoCommand.ExecuteReader();
|
|
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
while (tableInfoReader.Read())
|
|
columns.Add(tableInfoReader.GetString(1));
|
|
|
|
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');";
|
|
using var rollTableInfoReader = rollTableInfoCommand.ExecuteReader();
|
|
var rollColumns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
while (rollTableInfoReader.Read())
|
|
rollColumns.Add(rollTableInfoReader.GetString(1));
|
|
|
|
Assert.Contains("Dice", rollColumns);
|
|
|
|
using var usersTableInfoCommand = verifyConnection.CreateCommand();
|
|
usersTableInfoCommand.CommandText = "PRAGMA table_info('Users');";
|
|
using var usersTableInfoReader = usersTableInfoCommand.ExecuteReader();
|
|
var usersColumns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
while (usersTableInfoReader.Read())
|
|
usersColumns.Add(usersTableInfoReader.GetString(1));
|
|
|
|
Assert.Contains("Roles", usersColumns);
|
|
|
|
using var usersRoleCommand = verifyConnection.CreateCommand();
|
|
usersRoleCommand.CommandText = "SELECT Roles FROM Users WHERE UsernameNormalized = 'LEGACY-ADMIN';";
|
|
var roles = Convert.ToString(usersRoleCommand.ExecuteScalar());
|
|
Assert.Equal("admin", roles);
|
|
|
|
using var charactersTableInfoCommand = verifyConnection.CreateCommand();
|
|
charactersTableInfoCommand.CommandText = "PRAGMA table_info('Characters');";
|
|
using var charactersTableInfoReader = charactersTableInfoCommand.ExecuteReader();
|
|
var campaignIdNotNull = true;
|
|
while (charactersTableInfoReader.Read())
|
|
{
|
|
if (!string.Equals(charactersTableInfoReader.GetString(1), "CampaignId", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
campaignIdNotNull = charactersTableInfoReader.GetInt32(3) == 1;
|
|
break;
|
|
}
|
|
|
|
Assert.False(campaignIdNotNull);
|
|
|
|
using var historyCommand = verifyConnection.CreateCommand();
|
|
historyCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260226084000_InitialSchema';";
|
|
var historyCount = Convert.ToInt32(historyCommand.ExecuteScalar());
|
|
Assert.Equal(1, historyCount);
|
|
|
|
using var modelSyncHistoryCommand = verifyConnection.CreateCommand();
|
|
modelSyncHistoryCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260226090000_ModelSync';";
|
|
var modelSyncHistoryCount = Convert.ToInt32(modelSyncHistoryCommand.ExecuteScalar());
|
|
Assert.Equal(1, modelSyncHistoryCount);
|
|
|
|
using var rollDiceHistoryCommand = verifyConnection.CreateCommand();
|
|
rollDiceHistoryCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260226100000_AddRollLogDice';";
|
|
var rollDiceHistoryCount = Convert.ToInt32(rollDiceHistoryCommand.ExecuteScalar());
|
|
Assert.Equal(1, rollDiceHistoryCount);
|
|
|
|
using var rolesHistoryCommand = verifyConnection.CreateCommand();
|
|
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 AuthorizationRolesAndCampaignDeletion_MigrationScript_DoesNotMixUsersSqlIntoCharactersRebuild()
|
|
{
|
|
var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-migration-script-{Guid.NewGuid():N}.db");
|
|
var options = new DbContextOptionsBuilder<RpgRollerDbContext>().UseSqlite($"Data Source={dbPath}").Options;
|
|
|
|
using var db = new RpgRollerDbContext(options);
|
|
var migrator = db.GetService<IMigrator>();
|
|
var script = migrator.GenerateScript(
|
|
fromMigration: "20260226131003_AddSkillGroupPrototypes",
|
|
toMigration: "20260226160859_AddAuthorizationRolesAndCampaignDeletion");
|
|
|
|
Assert.Contains("""ALTER TABLE "Users" ADD "Roles" TEXT NOT NULL DEFAULT 'admin';""", script);
|
|
Assert.Contains("""CREATE TABLE "ef_temp_Characters" (""", script);
|
|
Assert.DoesNotContain("UPDATE Users", script);
|
|
}
|
|
|
|
[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.Logging.AddFilter("Microsoft.AspNetCore", LogLevel.Warning);
|
|
builder.Logging.AddFilter("Microsoft.EntityFrameworkCore", LogLevel.Warning);
|
|
builder.Logging.AddFilter("Microsoft.Hosting", LogLevel.Warning);
|
|
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);
|
|
}
|
|
}
|