using Elsa.Common.Multitenancy;
using Elsa.Secrets.Models;
namespace Elsa.Secrets.UnitTests;
///
/// Shared File / InMemory / EF Core store-contract assertions for secret repositories.
/// Covers per-tenant name uniqueness and ambient-tenant isolation on the existing
/// surface.
///
public abstract class SecretRepositoryConformanceTests
{
protected abstract Task CreateScenarioAsync();
[Fact]
public async Task TwoTenantsCanOwnTheSameSecretName()
{
await using var scenario = await CreateScenarioAsync();
using (scenario.UseTenant("tenant-a"))
{
var secretA = CreateSecret("SMTP:PASSWORD", "Tenant A");
await scenario.Repository.AddAsync(secretA);
Assert.Equal("tenant-a", secretA.TenantId);
await Assert.ThrowsAsync(() =>
scenario.Repository.AddAsync(CreateSecret("smtp:password", "Duplicate A")));
}
using (scenario.UseTenant("tenant-b"))
{
var secretB = CreateSecret("smtp:password", "Tenant B");
await scenario.Repository.AddAsync(secretB);
Assert.Equal("tenant-b", secretB.TenantId);
Assert.Equal("Tenant B", (await scenario.Repository.GetAsync("SMTP:PASSWORD"))!.DisplayName);
}
using (scenario.UseTenant("tenant-a"))
Assert.Equal("Tenant A", (await scenario.Repository.GetAsync("smtp:password"))!.DisplayName);
}
[Fact]
public async Task GetDoesNotCrossReadAnotherTenant()
{
await using var scenario = await CreateScenarioAsync();
using (scenario.UseTenant("tenant-a"))
await scenario.Repository.AddAsync(CreateSecret("smtp:password", "Tenant A"));
using (scenario.UseTenant("tenant-b"))
{
Assert.Null(await scenario.Repository.GetAsync("smtp:password"));
await scenario.Repository.AddAsync(CreateSecret("smtp:password", "Tenant B"));
}
using (scenario.UseTenant("tenant-a"))
{
var loaded = await scenario.Repository.GetAsync("SMTP:PASSWORD");
Assert.NotNull(loaded);
Assert.Equal("Tenant A", loaded.DisplayName);
Assert.Equal("tenant-a", loaded.TenantId);
}
}
[Fact]
public async Task ListIsTenantScoped()
{
await using var scenario = await CreateScenarioAsync();
using (scenario.UseTenant("tenant-a"))
{
await scenario.Repository.AddAsync(CreateSecret("smtp:password", "Tenant A"));
await scenario.Repository.AddAsync(CreateSecret("api:key", "Key A"));
}
using (scenario.UseTenant("tenant-b"))
{
await scenario.Repository.AddAsync(CreateSecret("smtp:password", "Tenant B"));
var listedB = await scenario.Repository.ListAsync();
Assert.Equal("Tenant B", Assert.Single(listedB).DisplayName);
}
using (scenario.UseTenant("tenant-a"))
{
var listedA = (await scenario.Repository.ListAsync()).OrderBy(x => x.Name).ToList();
Assert.Equal(["api:key", "smtp:password"], listedA.Select(x => x.Name).ToList());
Assert.All(listedA, secret => Assert.Equal("tenant-a", secret.TenantId));
}
}
[Fact]
public async Task DeletedReplaceStaysPerTenant()
{
await using var scenario = await CreateScenarioAsync();
using (scenario.UseTenant("tenant-a"))
{
await scenario.Repository.SaveAsync(CreateSecret("smtp:password", "Deleted A", SecretStatus.Deleted));
Assert.True(await scenario.Repository.TryAddOrReplaceDeletedAsync(CreateSecret("SMTP:PASSWORD", "Replacement A")));
Assert.Equal("Replacement A", (await scenario.Repository.GetAsync("smtp:password"))!.DisplayName);
}
using (scenario.UseTenant("tenant-b"))
{
Assert.True(await scenario.Repository.TryAddOrReplaceDeletedAsync(CreateSecret("smtp:password", "Tenant B")));
Assert.Equal("Tenant B", (await scenario.Repository.GetAsync("smtp:password"))!.DisplayName);
}
using (scenario.UseTenant("tenant-a"))
Assert.Equal("Replacement A", (await scenario.Repository.GetAsync("smtp:password"))!.DisplayName);
}
[Fact]
public async Task DefaultTenantRejectsDuplicateNames()
{
await using var scenario = await CreateScenarioAsync();
using (scenario.UseTenant(Tenant.DefaultTenantId))
{
var secret = CreateSecret("smtp:password", "Default");
await scenario.Repository.AddAsync(secret);
Assert.Equal(Tenant.DefaultTenantId, secret.TenantId);
await Assert.ThrowsAsync(() =>
scenario.Repository.AddAsync(CreateSecret("SMTP:PASSWORD", "Duplicate")));
}
}
private static Secret CreateSecret(string name, string displayName, SecretStatus status = SecretStatus.Active) =>
new()
{
Name = name,
DisplayName = displayName,
Status = status
};
}
public sealed class InMemorySecretRepositoryConformanceTests : SecretRepositoryConformanceTests
{
protected override Task CreateScenarioAsync() =>
SecretRepositoryScenario.CreateInMemoryAsync();
}
public sealed class FileSecretRepositoryConformanceTests : SecretRepositoryConformanceTests
{
protected override Task CreateScenarioAsync() =>
SecretRepositoryScenario.CreateFileAsync();
}
public sealed class SqliteSecretRepositoryConformanceTests : SecretRepositoryConformanceTests
{
protected override Task CreateScenarioAsync() =>
SecretRepositoryScenario.CreateSqliteAsync();
}