230 lines
9 KiB
C#
230 lines
9 KiB
C#
using Elsa.Secrets.Models;
|
|
using Elsa.Secrets.Options;
|
|
using Elsa.Secrets.Repositories;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Xunit;
|
|
|
|
namespace Elsa.Secrets.UnitTests;
|
|
|
|
public class SecretStoreTests
|
|
{
|
|
[Fact]
|
|
public void Registries_ExposeBuiltInTypesAndStores()
|
|
{
|
|
var fixture = new SecretTestFixture();
|
|
|
|
Assert.Contains(fixture.TypeRegistry.List(), x => x.Name == SecretTypeNames.Text);
|
|
Assert.Contains(fixture.TypeRegistry.List(), x => x.Name == SecretTypeNames.RsaKey);
|
|
Assert.Contains(fixture.TypeRegistry.List(), x => x.Name == SecretTypeNames.X509Certificate);
|
|
Assert.Contains(fixture.StoreRegistry.List(), x => x.Name == SecretStoreNames.Encrypted);
|
|
Assert.Contains(fixture.StoreRegistry.List(), x => x.Name == SecretStoreNames.Configuration);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConfigurationStore_ResolvesConfiguredValue()
|
|
{
|
|
var configuration = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?> { ["Elsa:Secrets:SmtpPassword"] = "configured-secret" })
|
|
.Build();
|
|
var fixture = new SecretTestFixture(configuration);
|
|
|
|
await fixture.Manager.CreateAsync(new CreateSecretRequest
|
|
{
|
|
Name = "smtp:password",
|
|
StoreName = SecretStoreNames.Configuration,
|
|
ConfigurationKey = "SmtpPassword"
|
|
});
|
|
|
|
var value = await fixture.Resolver.ResolveAsync("smtp:password");
|
|
|
|
Assert.Equal("configured-secret", value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConfigurationStore_FallsBackToRootConfigurationKey()
|
|
{
|
|
var configuration = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?> { ["SmtpPassword"] = "root-configured-secret" })
|
|
.Build();
|
|
var fixture = new SecretTestFixture(configuration);
|
|
|
|
var secret = await fixture.Manager.CreateAsync(new CreateSecretRequest
|
|
{
|
|
Name = "smtp:password",
|
|
StoreName = SecretStoreNames.Configuration,
|
|
ConfigurationKey = " SmtpPassword "
|
|
});
|
|
var value = await fixture.Resolver.ResolveAsync("smtp:password");
|
|
|
|
Assert.Equal("root-configured-secret", value);
|
|
Assert.Null(secret.Versions.Single().Payload.Value);
|
|
Assert.Equal("SmtpPassword", secret.Versions.Single().Payload.Metadata["configurationKey"]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConfigurationStore_RotateAsync_UsesReplacementConfigurationKey()
|
|
{
|
|
var configuration = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["Elsa:Secrets:OldPassword"] = "old-configured-secret",
|
|
["Elsa:Secrets:NewPassword"] = "new-configured-secret"
|
|
})
|
|
.Build();
|
|
var fixture = new SecretTestFixture(configuration);
|
|
|
|
await fixture.Manager.CreateAsync(new CreateSecretRequest
|
|
{
|
|
Name = "smtp:password",
|
|
StoreName = SecretStoreNames.Configuration,
|
|
ConfigurationKey = "OldPassword"
|
|
});
|
|
await fixture.Manager.RotateAsync("smtp:password", new RotateSecretRequest { ConfigurationKey = "NewPassword" });
|
|
var value = await fixture.Resolver.ResolveAsync("smtp:password");
|
|
|
|
Assert.Equal("new-configured-secret", value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConfigurationStore_TestAsync_ReturnsFalseWhenConfiguredValueIsMissing()
|
|
{
|
|
var fixture = new SecretTestFixture();
|
|
await fixture.Manager.CreateAsync(new CreateSecretRequest
|
|
{
|
|
Name = "smtp:password",
|
|
StoreName = SecretStoreNames.Configuration,
|
|
ConfigurationKey = "MissingPassword"
|
|
});
|
|
|
|
var result = await fixture.Manager.TestAsync("smtp:password");
|
|
|
|
Assert.False(result.Succeeded);
|
|
Assert.Equal("Secret value is unavailable.", result.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public void Registries_Throw_WhenTypeOrStoreIsMissing()
|
|
{
|
|
var fixture = new SecretTestFixture();
|
|
|
|
var missingType = Assert.Throws<InvalidOperationException>(() => fixture.TypeRegistry.Get("missing-type"));
|
|
var missingStore = Assert.Throws<InvalidOperationException>(() => fixture.StoreRegistry.Get("missing-store"));
|
|
|
|
Assert.Contains("missing-type", missingType.Message);
|
|
Assert.Contains("missing-store", missingStore.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FileRepository_PersistsSecretAggregate()
|
|
{
|
|
await WithFileRepositoryAsync(async (repository, path) =>
|
|
{
|
|
var secret = new Secret
|
|
{
|
|
Name = "smtp:password",
|
|
DisplayName = "SMTP password",
|
|
Tags = ["API-Key"],
|
|
Versions = { new SecretVersion { Version = 1, Payload = new SecretPayload { Value = "stored", Metadata = { ["ProtectedValue"] = "ciphertext" } } } }
|
|
};
|
|
|
|
await repository.AddAsync(secret);
|
|
|
|
var reloadedRepository = new FileSecretRepository(Microsoft.Extensions.Options.Options.Create(new SecretsOptions { RepositoryFilePath = path }));
|
|
var reloaded = await reloadedRepository.GetAsync("smtp:password");
|
|
|
|
Assert.NotNull(reloaded);
|
|
Assert.Equal("SMTP password", reloaded.DisplayName);
|
|
Assert.Contains("api-key", reloaded.Tags);
|
|
Assert.True(reloaded.Versions.Single().Payload.Metadata.ContainsKey("protectedvalue"));
|
|
Assert.Equal(1, reloaded.Versions.Single().Version);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FileRepository_SaveAsync_AddsAndUpdatesSecret()
|
|
{
|
|
await WithFileRepositoryAsync(async (repository, _) =>
|
|
{
|
|
await repository.SaveAsync(new Secret { Name = "smtp:password", DisplayName = "SMTP password" });
|
|
await repository.SaveAsync(new Secret { Name = "smtp:password", DisplayName = "Updated password" });
|
|
var reloaded = await repository.GetAsync("smtp:password");
|
|
|
|
Assert.NotNull(reloaded);
|
|
Assert.Equal("Updated password", reloaded.DisplayName);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FileRepository_TryAddOrReplaceDeletedAsync_ReplacesOnlyDeletedSecret()
|
|
{
|
|
await WithFileRepositoryAsync(async (repository, _) =>
|
|
{
|
|
await repository.AddAsync(new Secret { Name = "smtp:password", DisplayName = "SMTP password" });
|
|
|
|
var activeReplacementResult = await repository.TryAddOrReplaceDeletedAsync(new Secret { Name = "SMTP:PASSWORD", DisplayName = "Active replacement" });
|
|
await repository.SaveAsync(new Secret { Name = "smtp:password", DisplayName = "Deleted password", Status = SecretStatus.Deleted });
|
|
var deletedReplacementResult = await repository.TryAddOrReplaceDeletedAsync(new Secret { Name = "SMTP:PASSWORD", DisplayName = "Replacement password" });
|
|
var reloaded = await repository.GetAsync("smtp:password");
|
|
|
|
Assert.False(activeReplacementResult);
|
|
Assert.True(deletedReplacementResult);
|
|
Assert.NotNull(reloaded);
|
|
Assert.Equal("Replacement password", reloaded.DisplayName);
|
|
Assert.Equal(SecretStatus.Active, reloaded.Status);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FileRepository_RecoversFromCorruptJson()
|
|
{
|
|
await WithFileRepositoryAsync(async (repository, path) =>
|
|
{
|
|
await File.WriteAllTextAsync(path, "{not-valid-json");
|
|
|
|
var secrets = await repository.ListAsync();
|
|
await repository.AddAsync(new Secret { Name = "smtp:password", DisplayName = "SMTP password" });
|
|
var reloaded = await repository.GetAsync("smtp:password");
|
|
|
|
Assert.Empty(secrets);
|
|
Assert.NotNull(reloaded);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InMemoryRepository_ReturnsCopies()
|
|
{
|
|
var repository = new InMemorySecretRepository();
|
|
await repository.AddAsync(new Secret
|
|
{
|
|
Name = "smtp:password",
|
|
DisplayName = "SMTP password",
|
|
Versions = { new SecretVersion { Version = 1, Payload = new SecretPayload { Metadata = { ["protectedValue"] = "ciphertext" } } } }
|
|
});
|
|
|
|
var loaded = await repository.GetAsync("smtp:password");
|
|
loaded!.Versions.Clear();
|
|
loaded.DisplayName = "Changed";
|
|
|
|
var reloaded = await repository.GetAsync("smtp:password");
|
|
|
|
Assert.Equal("SMTP password", reloaded!.DisplayName);
|
|
Assert.Single(reloaded.Versions);
|
|
Assert.True(reloaded.Versions.Single().Payload.Metadata.ContainsKey("protectedValue"));
|
|
}
|
|
|
|
private static async Task WithFileRepositoryAsync(Func<FileSecretRepository, string, Task> test)
|
|
{
|
|
var path = Path.Join(Path.GetTempPath(), $"elsa-secrets-{Guid.NewGuid():N}.json");
|
|
try
|
|
{
|
|
var repository = new FileSecretRepository(Microsoft.Extensions.Options.Options.Create(new SecretsOptions { RepositoryFilePath = path }));
|
|
await test(repository, path);
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(path))
|
|
File.Delete(path);
|
|
}
|
|
}
|
|
}
|