mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-08 06:05:02 -04:00
540 lines
21 KiB
C#
540 lines
21 KiB
C#
using allstarr.Core.Configuration;
|
|
using allstarr.Core.Jobs;
|
|
using allstarr.Core.Operations;
|
|
using allstarr.Core.Storage;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
using Microsoft.EntityFrameworkCore.Migrations;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
namespace allstarr.Tests;
|
|
|
|
public sealed class DurableStorageTests : IAsyncLifetime
|
|
{
|
|
private PostgresTestDatabase _database = null!;
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
_database = await PostgresTestDatabase.CreateAsync(useTemplate: false);
|
|
}
|
|
|
|
[Fact]
|
|
public void Options_RejectUnknownProviderInsteadOfFallingBack()
|
|
{
|
|
var options = new DurableStorageOptions
|
|
{
|
|
Provider = "automatic",
|
|
ConnectionString = _database.ConnectionString
|
|
};
|
|
|
|
var exception = Assert.Throws<InvalidOperationException>(() => options.ParseProvider());
|
|
|
|
Assert.Contains("Postgres", exception.Message, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("Sqlite", exception.Message, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostgresInitializer_AppliesCheckedInMigrationsAndReportsSchema()
|
|
{
|
|
var options = Options();
|
|
var state = new DurableStorageState(options);
|
|
var runtime = new OperationalRuntimeState();
|
|
using var traces = new PlatformTraceCollector();
|
|
await traces.StartAsync(CancellationToken.None);
|
|
var initializer = new DurableStorageInitializer(
|
|
Factory(),
|
|
options,
|
|
state,
|
|
NullLogger<DurableStorageInitializer>.Instance,
|
|
runtimeState: runtime);
|
|
|
|
await initializer.StartAsync(CancellationToken.None);
|
|
|
|
var snapshot = state.GetSnapshot();
|
|
Assert.Equal(DurableStorageReadiness.Ready, snapshot.Readiness);
|
|
await using var context = await Factory().CreateDbContextAsync();
|
|
Assert.Equal(context.Database.GetMigrations().Last(), snapshot.SchemaVersion);
|
|
Assert.True(await TableExists(context, "durable_jobs"));
|
|
Assert.True(await TableExists(context, "canonical_recordings"));
|
|
Assert.True(await TableExists(context, "provider_track_identities"));
|
|
Assert.True(await TableExists(context, "tenant_runtime_settings"));
|
|
Assert.Equal(1, runtime.GetSnapshot().MigrationAttempts);
|
|
Assert.Equal(0, runtime.GetSnapshot().MigrationFailures);
|
|
Assert.Contains(traces.GetSnapshot(), span =>
|
|
span.Operation == "storage.migrate" && !span.Failed);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoMigrateDisabled_WithPendingSchema_RemainsUnready()
|
|
{
|
|
var options = Options();
|
|
options.AutoMigrate = false;
|
|
var state = new DurableStorageState(options);
|
|
var initializer = new DurableStorageInitializer(
|
|
Factory(),
|
|
options,
|
|
state,
|
|
NullLogger<DurableStorageInitializer>.Instance);
|
|
|
|
await initializer.StartAsync(CancellationToken.None);
|
|
|
|
var snapshot = state.GetSnapshot();
|
|
Assert.Equal(DurableStorageReadiness.SchemaIncompatible, snapshot.Readiness);
|
|
Assert.Equal("schema_migration_required", snapshot.ErrorCode);
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Lane", "ReleaseCritical")]
|
|
public async Task Initializer_RejectsUnknownNewerMigrationWithoutChangingSchema()
|
|
{
|
|
await using (var context = await Factory().CreateDbContextAsync())
|
|
{
|
|
await context.Database.MigrateAsync();
|
|
await context.Database.ExecuteSqlRawAsync(
|
|
"INSERT INTO \"__EFMigrationsHistory\" (\"MigrationId\", \"ProductVersion\") " +
|
|
"VALUES ('99991231235959_FutureAllstarrSchema', '99.0.0')");
|
|
}
|
|
|
|
var options = Options();
|
|
var state = new DurableStorageState(options);
|
|
var runtime = new OperationalRuntimeState();
|
|
var initializer = new DurableStorageInitializer(
|
|
Factory(),
|
|
options,
|
|
state,
|
|
NullLogger<DurableStorageInitializer>.Instance,
|
|
runtimeState: runtime);
|
|
|
|
await initializer.StartAsync(CancellationToken.None);
|
|
|
|
var snapshot = state.GetSnapshot();
|
|
Assert.Equal(DurableStorageReadiness.SchemaIncompatible, snapshot.Readiness);
|
|
Assert.Equal(DurableSchemaCompatibility.UnsupportedVersionErrorCode, snapshot.ErrorCode);
|
|
Assert.Equal("99991231235959_FutureAllstarrSchema", snapshot.SchemaVersion);
|
|
Assert.Equal(1, runtime.GetSnapshot().MigrationFailures);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RestoreTargetVerifier_RejectsUnknownMigrationFromTargetItInspects()
|
|
{
|
|
await using (var context = await Factory().CreateDbContextAsync())
|
|
{
|
|
await context.Database.MigrateAsync();
|
|
await context.Database.ExecuteSqlRawAsync(
|
|
"INSERT INTO \"__EFMigrationsHistory\" (\"MigrationId\", \"ProductVersion\") " +
|
|
"VALUES ('99991231235959_FutureAllstarrSchema', '99.0.0')");
|
|
}
|
|
|
|
var exception = await Assert.ThrowsAsync<BackupVerificationException>(() =>
|
|
new DurableRestoreTargetVerifier().VerifyAsync(
|
|
DurableStorageProvider.Postgres,
|
|
_database.ConnectionString));
|
|
|
|
Assert.Contains("schema", exception.Message, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostgresAdditiveMigrations_ApplyCleanly()
|
|
{
|
|
await using var context = await Factory().CreateDbContextAsync();
|
|
await context.Database.MigrateAsync();
|
|
|
|
Assert.True(await TableExists(context, "provider_health_rollups"));
|
|
Assert.True(await ColumnExists(context, "durable_jobs", "MaxDeferrals"));
|
|
Assert.True(await ColumnExists(context, "durable_jobs", "PolicySnapshotJson"));
|
|
Assert.True(await ColumnExists(context, "durable_jobs", "RequestFingerprint"));
|
|
Assert.True(await ColumnExists(context, "outbox_messages", "MaxAttempts"));
|
|
Assert.True(await ColumnExists(context, "backups", "RestoreStatus"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SuggestedTargetMigration_NormalizesLegacyTargetlessRows()
|
|
{
|
|
await using var context = await Factory().CreateDbContextAsync();
|
|
var migrator = context.GetService<IMigrator>();
|
|
await migrator.MigrateAsync("20260727233000_AllowExternalAcceptedTrackMatches");
|
|
var now = DateTimeOffset.UtcNow;
|
|
var tenantId = Guid.CreateVersion7();
|
|
var userId = Guid.CreateVersion7();
|
|
var accountId = Guid.CreateVersion7();
|
|
var snapshotId = Guid.CreateVersion7();
|
|
context.AddRange(
|
|
new TenantRecord
|
|
{
|
|
Id = tenantId,
|
|
Slug = "legacy-suggestion",
|
|
Name = "Legacy suggestion",
|
|
CreatedAt = now
|
|
},
|
|
new PlatformUserRecord
|
|
{
|
|
Id = userId,
|
|
TenantId = tenantId,
|
|
DisplayName = "User",
|
|
Status = PlatformUserStatus.Active,
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
},
|
|
new ProviderAccountRecord
|
|
{
|
|
Id = accountId,
|
|
TenantId = tenantId,
|
|
OwnerUserId = userId,
|
|
ProviderId = "spotify",
|
|
DisplayName = "Spotify",
|
|
Scope = ProviderAccountScope.User,
|
|
Enabled = true,
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
});
|
|
await context.SaveChangesAsync();
|
|
context.ExternalMetadataSnapshots.Add(new ExternalMetadataSnapshotRecord
|
|
{
|
|
Id = snapshotId,
|
|
TenantId = tenantId,
|
|
OwnerUserId = userId,
|
|
ProviderAccountId = accountId,
|
|
LibraryScopeId = "music",
|
|
BackendInstanceId = "home",
|
|
BackendPrincipalId = "principal",
|
|
Protocol = "jellyfin",
|
|
ProviderId = "spotify",
|
|
ResourceKind = "track",
|
|
ExternalIdHash = new('a', 64),
|
|
SnapshotVersion = 1,
|
|
PayloadJson = "{}",
|
|
PayloadSha256 = new('b', 64),
|
|
CorrelationId = "legacy-suggestion",
|
|
RetrievedAt = now
|
|
});
|
|
await context.SaveChangesAsync();
|
|
context.TrackMatches.Add(new TrackMatchRecord
|
|
{
|
|
Id = Guid.CreateVersion7(),
|
|
TenantId = tenantId,
|
|
OwnerUserId = userId,
|
|
ExternalSnapshotId = snapshotId,
|
|
LibraryScopeId = "music",
|
|
State = TrackMatchState.Suggested,
|
|
Confidence = .8,
|
|
Threshold = .88,
|
|
DecisionVersion = 1,
|
|
PolicyVersion = "normalized-v6",
|
|
ReasonsJson = "[]",
|
|
CandidateResultsJson = "[]",
|
|
WarningsJson = "[]",
|
|
CorrelationId = "legacy-suggestion",
|
|
DecidedAt = now
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
await migrator.MigrateAsync();
|
|
context.ChangeTracker.Clear();
|
|
|
|
Assert.Equal(TrackMatchState.Unresolved, await context.TrackMatches
|
|
.Select(item => item.State)
|
|
.SingleAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ImportedPlaylistScheduleMigration_ActivatesLegacyLinksIdempotently()
|
|
{
|
|
await using var context = await Factory().CreateDbContextAsync();
|
|
var migrator = context.GetService<IMigrator>();
|
|
await migrator.MigrateAsync("20260727234500_AllowSuggestedTrackTargets");
|
|
var now = DateTimeOffset.UtcNow;
|
|
var tenantId = Guid.CreateVersion7();
|
|
var userId = Guid.CreateVersion7();
|
|
var accountId = Guid.CreateVersion7();
|
|
var scheduleId = Guid.CreateVersion7();
|
|
context.AddRange(
|
|
new TenantRecord
|
|
{
|
|
Id = tenantId,
|
|
Slug = "legacy-playlist",
|
|
Name = "Legacy playlist",
|
|
CreatedAt = now
|
|
},
|
|
new PlatformUserRecord
|
|
{
|
|
Id = userId,
|
|
TenantId = tenantId,
|
|
DisplayName = "User",
|
|
Status = PlatformUserStatus.Active,
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
},
|
|
new ProviderAccountRecord
|
|
{
|
|
Id = accountId,
|
|
TenantId = tenantId,
|
|
OwnerUserId = userId,
|
|
ProviderId = "spotify",
|
|
DisplayName = "Spotify",
|
|
Scope = ProviderAccountScope.User,
|
|
Enabled = true,
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
},
|
|
new JobScheduleRecord
|
|
{
|
|
Id = scheduleId,
|
|
TenantId = tenantId,
|
|
OwnerUserId = userId,
|
|
LibraryScopeId = "music",
|
|
JobType = DurableScheduleEngine.PlaylistSyncJobType,
|
|
CronExpression = "0 8 * * *",
|
|
TimeZoneId = "UTC",
|
|
OverlapPolicy = ScheduleOverlapPolicy.Skip,
|
|
MisfirePolicy = ScheduleMisfirePolicy.RunOnce,
|
|
RetryPolicyJson = "{}",
|
|
PayloadTemplateJson = "{}",
|
|
Enabled = false,
|
|
CreatedAt = now,
|
|
UpdatedAt = now,
|
|
Revision = 1
|
|
});
|
|
await context.SaveChangesAsync();
|
|
var linkId = Guid.CreateVersion7();
|
|
await context.Database.ExecuteSqlInterpolatedAsync($"""
|
|
INSERT INTO playlist_links (
|
|
"Id", "TenantId", "OwnerUserId", "ProviderAccountId", "ScheduleId", "Enabled",
|
|
"LibraryScopeId", "SourceProviderId", "SourcePlaylistId", "SourcePlaylistIdHash",
|
|
"TargetProtocol", "TargetBackendInstanceId", "Mode", "MaterializationMode",
|
|
"MirrorStaleEntries", "PreserveManualEntries", "SyncName", "SyncDescription", "SyncArtwork",
|
|
"RuleVersion", "PolicyVersion", "CreatedAt", "UpdatedAt", "Revision")
|
|
VALUES (
|
|
{linkId}, {tenantId}, {userId}, {accountId}, {scheduleId}, {false},
|
|
{"music"}, {"spotify"}, {"source"}, {new string('a', 64)},
|
|
{"jellyfin"}, {"primary"}, {"Materialized"}, {"Reconcile"},
|
|
{false}, {true}, {true}, {true}, {true},
|
|
{"legacy-env-import-v1"}, {"legacy-env-import-v1"}, {now.UtcTicks}, {now.UtcTicks}, {1L})
|
|
""");
|
|
|
|
await migrator.MigrateAsync();
|
|
context.ChangeTracker.Clear();
|
|
|
|
var link = await context.PlaylistLinks.SingleAsync();
|
|
var schedule = await context.JobSchedules.SingleAsync();
|
|
Assert.True(link.Enabled);
|
|
Assert.True(schedule.Enabled);
|
|
Assert.NotNull(schedule.NextRunAt);
|
|
Assert.Equal(2, link.Revision);
|
|
Assert.Equal(2, schedule.Revision);
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Lane", "ReleaseCritical")]
|
|
public async Task ProbeCacheSnapshot_AppliesToFreshPostgres()
|
|
{
|
|
await using var context = await Factory().CreateDbContextAsync();
|
|
|
|
await context.Database.MigrateAsync();
|
|
|
|
Assert.Contains(
|
|
"20260724012448_ProbeCacheSnapshot",
|
|
await context.Database.GetAppliedMigrationsAsync());
|
|
Assert.Equal("boolean", await ColumnType(context, "playlist_links", "Enabled"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProbeCacheSnapshot_UpgradesLegacyIntegerEnabledColumn()
|
|
{
|
|
await using var context = await Factory().CreateDbContextAsync();
|
|
var migrator = context.GetService<IMigrator>();
|
|
await migrator.MigrateAsync("20260723233918_AddDownloadArtifactMediaFacts");
|
|
await context.Database.ExecuteSqlRawAsync("""
|
|
ALTER TABLE playlist_links
|
|
ALTER COLUMN "Enabled" DROP DEFAULT,
|
|
ALTER COLUMN "Enabled" TYPE integer USING (CASE WHEN "Enabled" THEN 1 ELSE 0 END),
|
|
ALTER COLUMN "Enabled" SET DEFAULT 1
|
|
""");
|
|
|
|
await migrator.MigrateAsync();
|
|
|
|
Assert.Equal("boolean", await ColumnType(context, "playlist_links", "Enabled"));
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Lane", "ReleaseCritical")]
|
|
public async Task DownloadedSongMappingRepair_UpgradesLegacyLowercaseIdWithoutDataLoss()
|
|
{
|
|
await using var context = await Factory().CreateDbContextAsync();
|
|
var migrator = context.GetService<IMigrator>();
|
|
await migrator.MigrateAsync("20260729010000_ActivateImportedPlaylistSchedules");
|
|
await context.Database.ExecuteSqlRawAsync(
|
|
"""ALTER TABLE downloaded_song_mappings RENAME COLUMN "Id" TO id""");
|
|
var mappingId = Guid.CreateVersion7();
|
|
await context.Database.ExecuteSqlInterpolatedAsync($"""
|
|
INSERT INTO downloaded_song_mappings
|
|
(id, "ProviderId", "ExternalId", "LocalPath", "Title", "Artist", "Album", "DownloadedAt", "Revision")
|
|
VALUES
|
|
({mappingId}, {"test"}, {"external"}, {"/music/test.flac"}, {"Test"}, {"Artist"}, {"Album"}, {DateTimeOffset.UtcNow.UtcTicks}, {1L})
|
|
""");
|
|
|
|
await migrator.MigrateAsync();
|
|
|
|
Assert.True(await ColumnExists(context, "downloaded_song_mappings", "Id"));
|
|
Assert.False(await ColumnExists(context, "downloaded_song_mappings", "id"));
|
|
Assert.Equal(mappingId, await context.DownloadedSongMappings.Select(item => item.Id).SingleAsync());
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Lane", "ReleaseCritical")]
|
|
public async Task SchemaCompatibility_RejectsCaseDivergentMigrationId()
|
|
{
|
|
await using var context = await Factory().CreateDbContextAsync();
|
|
await context.Database.MigrateAsync();
|
|
const string migration = "20260724012448_ProbeCacheSnapshot";
|
|
var divergent = migration.ToLowerInvariant();
|
|
await context.Database.ExecuteSqlInterpolatedAsync(
|
|
$"UPDATE \"__EFMigrationsHistory\" SET \"MigrationId\" = {divergent} WHERE \"MigrationId\" = {migration}");
|
|
|
|
var compatibility = await DurableSchemaCompatibility.InspectAsync(context);
|
|
|
|
Assert.Equal(DurableSchemaCompatibilityStatus.UnsupportedVersion, compatibility.Status);
|
|
Assert.Contains(migration, compatibility.MissingMigrations);
|
|
Assert.Contains(divergent, compatibility.UnknownMigrations);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OnboardingMigration_BackfillsExistingBackendIdentity()
|
|
{
|
|
await using var context = await Factory().CreateDbContextAsync();
|
|
var migrator = context.GetService<IMigrator>();
|
|
await migrator.MigrateAsync("20260726205440_VerifyPlaylistMaterialization");
|
|
var tenantId = Guid.CreateVersion7();
|
|
var userId = Guid.CreateVersion7();
|
|
var now = DateTimeOffset.UtcNow;
|
|
context.Tenants.Add(new TenantRecord
|
|
{
|
|
Id = tenantId,
|
|
Slug = "onboarding-upgrade",
|
|
Name = "Onboarding upgrade",
|
|
CreatedAt = now
|
|
});
|
|
context.Users.Add(new PlatformUserRecord
|
|
{
|
|
Id = userId,
|
|
TenantId = tenantId,
|
|
DisplayName = "Existing user",
|
|
Status = PlatformUserStatus.Active,
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
});
|
|
context.BackendIdentities.Add(new BackendIdentityRecord
|
|
{
|
|
Id = Guid.CreateVersion7(),
|
|
TenantId = tenantId,
|
|
UserId = userId,
|
|
BackendType = "jellyfin",
|
|
BackendInstanceId = "primary",
|
|
PrincipalId = "existing-user",
|
|
CreatedAt = now,
|
|
LastSeenAt = now
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
await migrator.MigrateAsync();
|
|
|
|
var state = await context.OnboardingStates.SingleAsync();
|
|
Assert.Equal(OnboardingStateService.SchemaVersion, state.SchemaVersion);
|
|
Assert.Equal("schema-backfill", state.CompletionSource);
|
|
Assert.NotNull(state.CompletedAt);
|
|
Assert.Contains(
|
|
OnboardingStateService.BackendIdentityStep,
|
|
state.CompletedStepsJson,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UnavailablePostgres_NeverCreatesFallbackStorage()
|
|
{
|
|
var options = new DurableStorageOptions
|
|
{
|
|
Provider = "Postgres",
|
|
ConnectionString =
|
|
"Host=127.0.0.1;Port=1;Database=allstarr;Username=allstarr;Password=test;Timeout=1;Command Timeout=1",
|
|
ConnectionRetryCount = 0,
|
|
AutoMigrate = true
|
|
};
|
|
var state = new DurableStorageState(options);
|
|
var dbOptions = new DbContextOptionsBuilder<AllstarrDbContext>()
|
|
.UseNpgsql(options.ConnectionString)
|
|
.Options;
|
|
var initializer = new DurableStorageInitializer(
|
|
new TestDbContextFactory(dbOptions),
|
|
options,
|
|
state,
|
|
NullLogger<DurableStorageInitializer>.Instance);
|
|
|
|
await initializer.StartAsync(CancellationToken.None);
|
|
|
|
var snapshot = state.GetSnapshot();
|
|
Assert.Equal(DurableStorageProvider.Postgres, snapshot.Provider);
|
|
Assert.Equal(DurableStorageReadiness.Unavailable, snapshot.Readiness);
|
|
Assert.Equal("database_initialization_failed", snapshot.ErrorCode);
|
|
}
|
|
|
|
[Fact]
|
|
public void CheckedInMigration_GeneratesNativePostgresSql()
|
|
{
|
|
using var context = new AllstarrDbContext(_database.Options);
|
|
var script = context.GetService<IMigrator>().GenerateScript();
|
|
|
|
Assert.Contains("CREATE TABLE tenants", script, StringComparison.OrdinalIgnoreCase);
|
|
Assert.Contains("CREATE TABLE canonical_recordings", script, StringComparison.OrdinalIgnoreCase);
|
|
Assert.Contains("uuid", script, StringComparison.OrdinalIgnoreCase);
|
|
Assert.Contains("bytea", script, StringComparison.OrdinalIgnoreCase);
|
|
Assert.DoesNotContain(" BLOB", script, StringComparison.OrdinalIgnoreCase);
|
|
Assert.DoesNotContain("AUTOINCREMENT", script, StringComparison.OrdinalIgnoreCase);
|
|
Assert.False(context.Database.HasPendingModelChanges());
|
|
}
|
|
|
|
private DurableStorageOptions Options() => new()
|
|
{
|
|
Provider = "Postgres",
|
|
ConnectionString = _database.ConnectionString,
|
|
AutoMigrate = true,
|
|
ConnectionRetryCount = 0
|
|
};
|
|
|
|
private TestDbContextFactory Factory() => new(_database.Options);
|
|
|
|
private static async Task<bool> TableExists(AllstarrDbContext context, string table) =>
|
|
await context.Database.SqlQuery<bool>(
|
|
$"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = {table}) AS \"Value\"")
|
|
.SingleAsync();
|
|
|
|
private static async Task<bool> ColumnExists(
|
|
AllstarrDbContext context,
|
|
string table,
|
|
string column) =>
|
|
await context.Database.SqlQuery<bool>(
|
|
$"SELECT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = {table} AND column_name = {column}) AS \"Value\"")
|
|
.SingleAsync();
|
|
|
|
private static async Task<string> ColumnType(
|
|
AllstarrDbContext context,
|
|
string table,
|
|
string column) =>
|
|
await context.Database.SqlQuery<string>(
|
|
$"SELECT data_type AS \"Value\" FROM information_schema.columns WHERE table_schema = 'public' AND table_name = {table} AND column_name = {column}")
|
|
.SingleAsync();
|
|
|
|
public async Task DisposeAsync()
|
|
{
|
|
await _database.DisposeAsync();
|
|
}
|
|
|
|
private sealed class TestDbContextFactory(DbContextOptions<AllstarrDbContext> options)
|
|
: IDbContextFactory<AllstarrDbContext>
|
|
{
|
|
public AllstarrDbContext CreateDbContext() => new(options);
|
|
|
|
public Task<AllstarrDbContext> CreateDbContextAsync(
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromResult(new AllstarrDbContext(options));
|
|
}
|
|
}
|