Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/Marten.Testing/Harness/TenantDatabases.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#nullable enable
using System.Threading.Tasks;
using Npgsql;
using Weasel.Postgresql;
using Weasel.Postgresql.Migrations;

namespace Marten.Testing.Harness
{
/// <summary>
/// Provisions per-tenant databases on the test PostgreSQL server for
/// database-per-tenant multi-tenancy tests. Databases are created once and
/// reused across runs (creation is expensive); callers are responsible for
/// resetting schemas or data inside them.
/// </summary>
public static class TenantDatabases
{
/// <summary>
/// Ensure the named database exists on the server behind
/// <paramref name="conn"/> (an open connection to the master test
/// database) and return a connection string targeting it.
/// </summary>
public static async Task<string> CreateIfNotExistsAsync(NpgsqlConnection conn, string databaseName)
{
if (!await conn.DatabaseExists(databaseName))
{
try
{
await new DatabaseSpecification().BuildDatabase(conn, databaseName);
}
catch (PostgresException e) when (e.SqlState is PostgresErrorCodes.DuplicateDatabase
or PostgresErrorCodes.UniqueViolation)
{
// The test assemblies can run once per target framework, concurrently;
// the check-then-create above is not atomic, so the loser of that race
// sees the database it wanted already there.
}
}

return ConnectionStringFor(databaseName);
}

/// <summary>
/// A connection string for the named database on the test server.
/// </summary>
public static string ConnectionStringFor(string databaseName)
{
return new NpgsqlConnectionStringBuilder(ConnectionSource.ConnectionString)
{
Database = databaseName
}.ConnectionString;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,15 @@

namespace MultiTenancyTests;

[CollectionDefinition("multi-tenancy", DisableParallelization = true)]
[Collection("multi-tenancy")]
public class DocumentStore_IMartenStorage_implementation : IAsyncLifetime
{
private IHost _host;
private IDocumentStore theStore;

private async Task<string> CreateDatabaseIfNotExists(NpgsqlConnection conn, string databaseName)
private static async Task<string> CreateDatabaseIfNotExists(NpgsqlConnection conn, string databaseName)
{
var builder = new NpgsqlConnectionStringBuilder(ConnectionSource.ConnectionString);

var exists = await conn.DatabaseExists(databaseName);
if (!exists)
{
await new DatabaseSpecification().BuildDatabase(conn, databaseName);
}

builder.Database = databaseName;

var connectionString = builder.ConnectionString;
var connectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, databaseName);

await SchemaUtils.DropSchema(connectionString, "multi_tenancy");
await SchemaUtils.DropSchema(connectionString, "mt_events");
Expand Down
9 changes: 9 additions & 0 deletions src/MultiTenancyTests/MultiTenancyCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Xunit;

namespace MultiTenancyTests;

// The single definition of the "multi-tenancy" collection. The classes in this
// collection all provision real per-tenant databases against the shared test
// server, so they must never run concurrently with each other.
[CollectionDefinition("multi-tenancy", DisableParallelization = true)]
public class MultiTenancyCollection;
3 changes: 3 additions & 0 deletions src/MultiTenancyTests/MultiTenancyTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@
<Compile Include="..\Marten.Testing\Harness\StoreContext.cs">
<Link>Harness\StoreContext.cs</Link>
</Compile>
<Compile Include="..\Marten.Testing\Harness\TenantDatabases.cs">
<Link>Harness\TenantDatabases.cs</Link>
</Compile>
<Compile Include="..\Marten.Testing\Harness\StoreFixture.cs">
<Link>Harness\StoreFixture.cs</Link>
</Compile>
Expand Down
2 changes: 1 addition & 1 deletion src/MultiTenancyTests/SingleServerMultiTenancyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

namespace MultiTenancyTests;

[CollectionDefinition("multi-tenancy", DisableParallelization = true)]
[Collection("multi-tenancy")]
public class SingleServerMultiTenancyTests: IAsyncLifetime
{
private DefaultNpgsqlDataSourceFactory dataSourceFactory = new();
Expand Down
25 changes: 2 additions & 23 deletions src/MultiTenancyTests/projection_statuses_per_database.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ public async ValueTask InitializeAsync()
await using var conn = new NpgsqlConnection(ConnectionSource.ConnectionString);
await conn.OpenAsync();
await conn.DropSchemaAsync(MasterSchema);
var tenantA = await CreateDatabaseIfNotExists(conn, TenantA);
var tenantB = await CreateDatabaseIfNotExists(conn, TenantB);
var tenantA = await TenantDatabases.CreateIfNotExistsAsync(conn, TenantA);
var tenantB = await TenantDatabases.CreateIfNotExistsAsync(conn, TenantB);
await conn.CloseAsync();

// Each run starts from sequence 0 so the assertions below are exact.
Expand Down Expand Up @@ -170,27 +170,6 @@ private static long SequenceFor(System.Collections.Generic.IReadOnlyList<Project
private static long HeadFor(System.Collections.Generic.IReadOnlyList<ProjectionStatus> statuses)
=> statuses.Single(x => x.ProjectionName == nameof(WidgetTally)).Shards.Single().EventStoreSequence;

private static async Task<string> CreateDatabaseIfNotExists(NpgsqlConnection conn, string databaseName)
{
var builder = new NpgsqlConnectionStringBuilder(ConnectionSource.ConnectionString);
if (!await conn.DatabaseExists(databaseName))
{
try
{
await new DatabaseSpecification().BuildDatabase(conn, databaseName);
}
catch (PostgresException e) when (e.SqlState is PostgresErrorCodes.DuplicateDatabase
or PostgresErrorCodes.UniqueViolation)
{
// The test assembly runs once per target framework, concurrently; the check-then-create above
// is not atomic, so the loser of that race sees the database it wanted already there.
}
}

builder.Database = databaseName;
return builder.ConnectionString;
}

private static async Task DropSchemaAsync(string connectionString)
{
await using var conn = new NpgsqlConnection(connectionString);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

namespace MultiTenancyTests;

[CollectionDefinition("multi-tenancy", DisableParallelization = true)]
[Collection("multi-tenancy")]
public class using_bucketed_database_sharding_and_document_partitioning: IAsyncLifetime
{
private const int NumberOfPartitions = 4;
Expand All @@ -40,20 +40,6 @@ public class using_bucketed_database_sharding_and_document_partitioning: IAsyncL
private static readonly string TenantBeta = "tenant_beta";
private static readonly string TenantGamma = "tenant_gamma";

private async Task<string> CreateDatabaseIfNotExists(NpgsqlConnection conn, string databaseName)
{
var builder = new NpgsqlConnectionStringBuilder(ConnectionSource.ConnectionString);

var exists = await conn.DatabaseExists(databaseName);
if (!exists)
{
await new DatabaseSpecification().BuildDatabase(conn, databaseName);
}

builder.Database = databaseName;
return builder.ConnectionString;
}

public async ValueTask InitializeAsync()
{
await using var conn = new NpgsqlConnection(ConnectionSource.ConnectionString);
Expand All @@ -64,7 +50,7 @@ public async ValueTask InitializeAsync()

foreach (var name in _dbNames)
{
_connectionStrings[name] = await CreateDatabaseIfNotExists(conn, name);
_connectionStrings[name] = await TenantDatabases.CreateIfNotExistsAsync(conn, name);
}

_registry = BucketRegistry.EvenlySpreadOver(_dbNames);
Expand Down
81 changes: 18 additions & 63 deletions src/MultiTenancyTests/using_master_table_multi_tenancy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,6 @@ namespace MultiTenancyTests;

public class master_table_multi_tenancy_independent_auto_create
{
private async Task<string> CreateDatabaseIfNotExists(NpgsqlConnection conn, string databaseName)
{
var builder = new NpgsqlConnectionStringBuilder(ConnectionSource.ConnectionString);

var exists = await conn.DatabaseExists(databaseName);
if (!exists)
{
await new DatabaseSpecification().BuildDatabase(conn, databaseName);
}

builder.Database = databaseName;

return builder.ConnectionString;
}

[Fact]
public async Task can_still_create_own_tables()
{
Expand All @@ -45,10 +30,10 @@ public async Task can_still_create_own_tables()

await conn.DropSchemaAsync("tenants", TestContext.Current.CancellationToken);

var tenant1ConnectionString = await CreateDatabaseIfNotExists(conn, "tenant1");
var tenant2ConnectionString = await CreateDatabaseIfNotExists(conn, "tenant2");
var tenant3ConnectionString = await CreateDatabaseIfNotExists(conn, "tenant3");
var tenant4ConnectionString = await CreateDatabaseIfNotExists(conn, "tenant4");
var tenant1ConnectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, "tenant1");
var tenant2ConnectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, "tenant2");
var tenant3ConnectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, "tenant3");
var tenant4ConnectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, "tenant4");

await conn.CloseAsync();

Expand Down Expand Up @@ -86,7 +71,7 @@ public async Task can_still_create_own_tables()
}


[CollectionDefinition("multi-tenancy", DisableParallelization = true)]
[Collection("multi-tenancy")]
public class master_table_multi_tenancy_seeding : IAsyncLifetime
{
private IHost _host;
Expand All @@ -96,21 +81,6 @@ public class master_table_multi_tenancy_seeding : IAsyncLifetime
private string tenant3ConnectionString;
private string tenant4ConnectionString;

private async Task<string> CreateDatabaseIfNotExists(NpgsqlConnection conn, string databaseName)
{
var builder = new NpgsqlConnectionStringBuilder(ConnectionSource.ConnectionString);

var exists = await conn.DatabaseExists(databaseName);
if (!exists)
{
await new DatabaseSpecification().BuildDatabase(conn, databaseName);
}

builder.Database = databaseName;

return builder.ConnectionString;
}

public async ValueTask InitializeAsync()
{
await using var conn = new NpgsqlConnection(ConnectionSource.ConnectionString);
Expand All @@ -119,10 +89,10 @@ public async ValueTask InitializeAsync()
await conn.DropSchemaAsync("tenants");


tenant1ConnectionString = await CreateDatabaseIfNotExists(conn, "tenant1");
tenant2ConnectionString = await CreateDatabaseIfNotExists(conn, "tenant2");
tenant3ConnectionString = await CreateDatabaseIfNotExists(conn, "tenant3");
tenant4ConnectionString = await CreateDatabaseIfNotExists(conn, "tenant4");
tenant1ConnectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, "tenant1");
tenant2ConnectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, "tenant2");
tenant3ConnectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, "tenant3");
tenant4ConnectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, "tenant4");

_host = await Host.CreateDefaultBuilder()
.ConfigureServices(services =>
Expand Down Expand Up @@ -199,7 +169,7 @@ public async Task can_be_immediately_using_tenant_databases_that_were_seeded()

}

[CollectionDefinition("multi-tenancy", DisableParallelization = true)]
[Collection("multi-tenancy")]
public class using_master_table_multi_tenancy : IAsyncLifetime
{
private IHost _host;
Expand All @@ -209,21 +179,6 @@ public class using_master_table_multi_tenancy : IAsyncLifetime
private string tenant3ConnectionString;
private string tenant4ConnectionString;

private async Task<string> CreateDatabaseIfNotExists(NpgsqlConnection conn, string databaseName)
{
var builder = new NpgsqlConnectionStringBuilder(ConnectionSource.ConnectionString);

var exists = await conn.DatabaseExists(databaseName);
if (!exists)
{
await new DatabaseSpecification().BuildDatabase(conn, databaseName);
}

builder.Database = databaseName;

return builder.ConnectionString;
}

public async ValueTask InitializeAsync()
{
await using var conn = new NpgsqlConnection(ConnectionSource.ConnectionString);
Expand All @@ -232,10 +187,10 @@ public async ValueTask InitializeAsync()
await conn.DropSchemaAsync("tenants");


tenant1ConnectionString = await CreateDatabaseIfNotExists(conn, "tenant1");
tenant2ConnectionString = await CreateDatabaseIfNotExists(conn, "tenant2");
tenant3ConnectionString = await CreateDatabaseIfNotExists(conn, "tenant3");
tenant4ConnectionString = await CreateDatabaseIfNotExists(conn, "tenant4");
tenant1ConnectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, "tenant1");
tenant2ConnectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, "tenant2");
tenant3ConnectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, "tenant3");
tenant4ConnectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, "tenant4");

_host = await Host.CreateDefaultBuilder()
.ConfigureServices(services =>
Expand Down Expand Up @@ -282,10 +237,10 @@ public async Task run_describe()
await conn.DropSchemaAsync("tenants", TestContext.Current.CancellationToken);


tenant1ConnectionString = await CreateDatabaseIfNotExists(conn, "tenant1");
tenant2ConnectionString = await CreateDatabaseIfNotExists(conn, "tenant2");
tenant3ConnectionString = await CreateDatabaseIfNotExists(conn, "tenant3");
tenant4ConnectionString = await CreateDatabaseIfNotExists(conn, "tenant4");
tenant1ConnectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, "tenant1");
tenant2ConnectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, "tenant2");
tenant3ConnectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, "tenant3");
tenant4ConnectionString = await TenantDatabases.CreateIfNotExistsAsync(conn, "tenant4");


var result = await Host.CreateDefaultBuilder()
Expand Down
Loading
Loading