Skip to content

Commit 9af54ab

Browse files
committed
Add and expand EFCore multitenancy test coverage
Introduces new unit tests for EFCore multitenancy features, including options, dependency injection, entity configuration, metadata, migration runner, model cache key factory, model customizer, and save changes interceptor. Updates test project dependencies to include EFCore.Relational and EFCore.Sqlite. Adds InternalsVisibleTo attribute for test assembly access.
1 parent dbf2f22 commit 9af54ab

11 files changed

+843
-1
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("CleanArchitecture.Extensions.Multitenancy.EFCore.Tests")]

tests/CleanArchitecture.Extensions.Multitenancy.EFCore.Tests/CleanArchitecture.Extensions.Multitenancy.EFCore.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
</PackageReference>
1717
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
1818
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.0" />
19+
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.0" />
20+
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
1921
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
2022
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
2123
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using CleanArchitecture.Extensions.Multitenancy;
2+
using CleanArchitecture.Extensions.Multitenancy.EFCore.Factories;
3+
using CleanArchitecture.Extensions.Multitenancy.EFCore.Options;
4+
5+
namespace CleanArchitecture.Extensions.Multitenancy.EFCore.Tests;
6+
7+
public class DefaultTenantConnectionResolverTests
8+
{
9+
[Fact]
10+
public void ResolveConnectionString_uses_format_from_options()
11+
{
12+
var options = new EfCoreMultitenancyOptions
13+
{
14+
ConnectionStringFormat = "Server={0};"
15+
};
16+
17+
var resolver = new DefaultTenantConnectionResolver(Microsoft.Extensions.Options.Options.Create(options));
18+
19+
var connectionString = resolver.ResolveConnectionString(new TenantInfo("alpha"));
20+
21+
Assert.Equal("Server=alpha;", connectionString);
22+
}
23+
24+
[Fact]
25+
public void ResolveConnectionString_uses_provider_from_options()
26+
{
27+
var options = new EfCoreMultitenancyOptions
28+
{
29+
ConnectionStringProvider = _ => "provider"
30+
};
31+
32+
var resolver = new DefaultTenantConnectionResolver(Microsoft.Extensions.Options.Options.Create(options));
33+
34+
var connectionString = resolver.ResolveConnectionString(new TenantInfo("alpha"));
35+
36+
Assert.Equal("provider", connectionString);
37+
}
38+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using CleanArchitecture.Extensions.Multitenancy.EFCore.Abstractions;
2+
using CleanArchitecture.Extensions.Multitenancy.EFCore.Interceptors;
3+
using CleanArchitecture.Extensions.Multitenancy.EFCore.Migrations;
4+
using Microsoft.EntityFrameworkCore.Diagnostics;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.EntityFrameworkCore.Infrastructure;
7+
8+
namespace CleanArchitecture.Extensions.Multitenancy.EFCore.Tests;
9+
10+
public class DependencyInjectionExtensionsTests
11+
{
12+
[Fact]
13+
public void AddCleanArchitectureMultitenancyEfCore_registers_services()
14+
{
15+
var services = new ServiceCollection();
16+
17+
services.AddCleanArchitectureMultitenancyEfCore();
18+
19+
Assert.Contains(services, descriptor => descriptor.ServiceType == typeof(ITenantModelCustomizer));
20+
Assert.Contains(services, descriptor => descriptor.ServiceType == typeof(ITenantConnectionResolver));
21+
Assert.Contains(services, descriptor => descriptor.ServiceType == typeof(IModelCacheKeyFactory));
22+
Assert.Contains(services, descriptor => descriptor.ServiceType == typeof(TenantSaveChangesInterceptor));
23+
Assert.Contains(services, descriptor => descriptor.ServiceType == typeof(ISaveChangesInterceptor));
24+
}
25+
26+
[Fact]
27+
public void AddTenantDbContextFactory_registers_factory_and_runner()
28+
{
29+
var services = new ServiceCollection();
30+
31+
services.AddTenantDbContextFactory<TestDbContext>();
32+
33+
Assert.Contains(services, descriptor => descriptor.ServiceType == typeof(ITenantDbContextFactory<TestDbContext>));
34+
Assert.Contains(services, descriptor => descriptor.ServiceType == typeof(TenantMigrationRunner<TestDbContext>));
35+
}
36+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using CleanArchitecture.Extensions.Multitenancy;
2+
using CleanArchitecture.Extensions.Multitenancy.EFCore.Options;
3+
4+
namespace CleanArchitecture.Extensions.Multitenancy.EFCore.Tests;
5+
6+
public class EfCoreMultitenancyOptionsTests
7+
{
8+
[Fact]
9+
public void ResolveSchemaName_uses_provider_when_configured()
10+
{
11+
var options = new EfCoreMultitenancyOptions
12+
{
13+
SchemaNameProvider = _ => "custom"
14+
};
15+
16+
var schema = options.ResolveSchemaName(new TenantInfo("alpha"));
17+
18+
Assert.Equal("custom", schema);
19+
}
20+
21+
[Fact]
22+
public void ResolveSchemaName_uses_default_schema_for_null_tenant()
23+
{
24+
var options = new EfCoreMultitenancyOptions
25+
{
26+
DefaultSchema = "default"
27+
};
28+
29+
var schema = options.ResolveSchemaName(null);
30+
31+
Assert.Equal("default", schema);
32+
}
33+
34+
[Fact]
35+
public void ResolveSchemaName_formats_schema_for_tenant()
36+
{
37+
var options = new EfCoreMultitenancyOptions
38+
{
39+
SchemaNameFormat = "tenant_{0}"
40+
};
41+
42+
var schema = options.ResolveSchemaName(new TenantInfo("alpha"));
43+
44+
Assert.Equal("tenant_alpha", schema);
45+
}
46+
47+
[Fact]
48+
public void ResolveSchemaName_falls_back_to_default_when_format_missing()
49+
{
50+
var options = new EfCoreMultitenancyOptions
51+
{
52+
SchemaNameFormat = string.Empty,
53+
DefaultSchema = "fallback"
54+
};
55+
56+
var schema = options.ResolveSchemaName(new TenantInfo("alpha"));
57+
58+
Assert.Equal("fallback", schema);
59+
}
60+
61+
[Fact]
62+
public void ResolveConnectionString_uses_provider_when_configured()
63+
{
64+
var options = new EfCoreMultitenancyOptions
65+
{
66+
ConnectionStringProvider = _ => "provider"
67+
};
68+
69+
var connectionString = options.ResolveConnectionString(new TenantInfo("alpha"));
70+
71+
Assert.Equal("provider", connectionString);
72+
}
73+
74+
[Fact]
75+
public void ResolveConnectionString_formats_string_for_tenant()
76+
{
77+
var options = new EfCoreMultitenancyOptions
78+
{
79+
ConnectionStringFormat = "Server={0};"
80+
};
81+
82+
var connectionString = options.ResolveConnectionString(new TenantInfo("alpha"));
83+
84+
Assert.Equal("Server=alpha;", connectionString);
85+
}
86+
87+
[Fact]
88+
public void ResolveConnectionString_returns_null_for_missing_tenant()
89+
{
90+
var options = new EfCoreMultitenancyOptions
91+
{
92+
ConnectionStringFormat = "Server={0};"
93+
};
94+
95+
var connectionString = options.ResolveConnectionString(null);
96+
97+
Assert.Null(connectionString);
98+
}
99+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using CleanArchitecture.Extensions.Multitenancy.EFCore.Abstractions;
2+
using Microsoft.EntityFrameworkCore;
3+
4+
namespace CleanArchitecture.Extensions.Multitenancy.EFCore.Tests;
5+
6+
public class TenantEntityConfigurationTests
7+
{
8+
[Fact]
9+
public void Configure_adds_required_tenant_id_for_tenant_entities()
10+
{
11+
using var dbContext = new ConfigurationDbContext(CreateOptions());
12+
var entityType = dbContext.Model.FindEntityType(typeof(ConfigTenantEntity));
13+
14+
Assert.NotNull(entityType);
15+
16+
var property = entityType!.FindProperty("TenantId");
17+
18+
Assert.NotNull(property);
19+
Assert.False(property!.IsNullable);
20+
}
21+
22+
[Fact]
23+
public void Configure_adds_shadow_tenant_id_for_plain_entities()
24+
{
25+
using var dbContext = new ConfigurationDbContext(CreateOptions());
26+
var entityType = dbContext.Model.FindEntityType(typeof(ConfigPlainEntity));
27+
28+
Assert.NotNull(entityType);
29+
30+
var property = entityType!.FindProperty("TenantId");
31+
32+
Assert.NotNull(property);
33+
Assert.True(property!.IsShadowProperty());
34+
}
35+
36+
[Fact]
37+
public void Configure_skips_tenant_id_for_global_entities()
38+
{
39+
using var dbContext = new ConfigurationDbContext(CreateOptions());
40+
var entityType = dbContext.Model.FindEntityType(typeof(ConfigGlobalEntity));
41+
42+
Assert.NotNull(entityType);
43+
44+
var property = entityType!.FindProperty("TenantId");
45+
46+
Assert.Null(property);
47+
}
48+
49+
private static DbContextOptions<ConfigurationDbContext> CreateOptions()
50+
=> new DbContextOptionsBuilder<ConfigurationDbContext>()
51+
.UseInMemoryDatabase(Guid.NewGuid().ToString())
52+
.Options;
53+
54+
private sealed class ConfigurationDbContext : DbContext
55+
{
56+
public ConfigurationDbContext(DbContextOptions<ConfigurationDbContext> options)
57+
: base(options)
58+
{
59+
}
60+
61+
protected override void OnModelCreating(ModelBuilder modelBuilder)
62+
{
63+
modelBuilder.ApplyConfiguration(new ConfigTenantEntityConfiguration());
64+
modelBuilder.ApplyConfiguration(new ConfigPlainEntityConfiguration());
65+
modelBuilder.ApplyConfiguration(new ConfigGlobalEntityConfiguration());
66+
}
67+
}
68+
69+
private sealed class ConfigTenantEntityConfiguration : TenantEntityConfiguration<ConfigTenantEntity>
70+
{
71+
}
72+
73+
private sealed class ConfigPlainEntityConfiguration : TenantEntityConfiguration<ConfigPlainEntity>
74+
{
75+
}
76+
77+
private sealed class ConfigGlobalEntityConfiguration : TenantEntityConfiguration<ConfigGlobalEntity>
78+
{
79+
}
80+
81+
private sealed class ConfigTenantEntity : ITenantEntity
82+
{
83+
public int Id { get; set; }
84+
public string TenantId { get; set; } = string.Empty;
85+
}
86+
87+
private sealed class ConfigPlainEntity
88+
{
89+
public int Id { get; set; }
90+
}
91+
92+
private sealed class ConfigGlobalEntity : IGlobalEntity
93+
{
94+
public int Id { get; set; }
95+
}
96+
}

0 commit comments

Comments
 (0)