115 lines
4.3 KiB
C#
115 lines
4.3 KiB
C#
|
|
using System.Text.Json.Nodes;
|
||
|
|
using Microsoft.EntityFrameworkCore;
|
||
|
|
using w4c_workflows.Data;
|
||
|
|
using w4c_workflows.Services.Credentials;
|
||
|
|
using Xunit;
|
||
|
|
|
||
|
|
namespace w4c_workflows.Tests;
|
||
|
|
|
||
|
|
[Collection("WorkflowsPostgres")]
|
||
|
|
public class CredentialVaultTests
|
||
|
|
{
|
||
|
|
private readonly WorkflowsPostgresFixture _fixture;
|
||
|
|
|
||
|
|
public CredentialVaultTests(WorkflowsPostgresFixture fixture) => _fixture = fixture;
|
||
|
|
|
||
|
|
private static CredentialVault Vault() => new(new ReversibleTestCipher(), new CredentialTypeCatalog());
|
||
|
|
|
||
|
|
private static string Tenant() => "cred" + Guid.NewGuid().ToString("N")[..12];
|
||
|
|
|
||
|
|
private static JsonObject HeaderData(string value = "secret-value")
|
||
|
|
=> new() { ["name"] = "X-API-Key", ["value"] = value };
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Create_stores_ciphertext_and_resolve_returns_decrypted_data()
|
||
|
|
{
|
||
|
|
var tenant = Tenant();
|
||
|
|
await using var db = _fixture.CreateContext();
|
||
|
|
var vault = Vault();
|
||
|
|
|
||
|
|
var created = await vault.CreateAsync(db, tenant, "my-api", "httpHeaderAuth", HeaderData(), default);
|
||
|
|
|
||
|
|
var stored = await db.Credentials.SingleAsync(c => c.Id == created.Id);
|
||
|
|
Assert.DoesNotContain("secret-value", stored.EncryptedData);
|
||
|
|
|
||
|
|
var resolved = await vault.ResolveAsync(
|
||
|
|
db, tenant, new Dictionary<string, string> { ["httpAuth"] = "my-api" }, default);
|
||
|
|
|
||
|
|
Assert.Equal("httpHeaderAuth", resolved["httpAuth"].Type);
|
||
|
|
Assert.Equal("secret-value", resolved["httpAuth"].Data["value"]!.GetValue<string>());
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Resolve_accepts_an_id_reference()
|
||
|
|
{
|
||
|
|
var tenant = Tenant();
|
||
|
|
await using var db = _fixture.CreateContext();
|
||
|
|
var vault = Vault();
|
||
|
|
var created = await vault.CreateAsync(db, tenant, "by-id", "httpHeaderAuth", HeaderData(), default);
|
||
|
|
|
||
|
|
var resolved = await vault.ResolveAsync(
|
||
|
|
db, tenant, new Dictionary<string, string> { ["httpAuth"] = created.Id.ToString() }, default);
|
||
|
|
|
||
|
|
Assert.True(resolved.ContainsKey("httpAuth"));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Resolve_throws_when_the_credential_is_missing()
|
||
|
|
{
|
||
|
|
var tenant = Tenant();
|
||
|
|
await using var db = _fixture.CreateContext();
|
||
|
|
|
||
|
|
await Assert.ThrowsAsync<CredentialResolutionException>(() => Vault().ResolveAsync(
|
||
|
|
db, tenant, new Dictionary<string, string> { ["httpAuth"] = "ghost" }, default));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Required_fields_are_validated_on_create()
|
||
|
|
{
|
||
|
|
await using var db = _fixture.CreateContext();
|
||
|
|
|
||
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => Vault().CreateAsync(
|
||
|
|
db, Tenant(), "missing-value", "httpHeaderAuth", new JsonObject { ["name"] = "X-API-Key" }, default));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task An_unknown_type_is_rejected()
|
||
|
|
{
|
||
|
|
await using var db = _fixture.CreateContext();
|
||
|
|
|
||
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => Vault().CreateAsync(
|
||
|
|
db, Tenant(), "weird", "not-a-type", new JsonObject(), default));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Duplicate_names_are_rejected()
|
||
|
|
{
|
||
|
|
var tenant = Tenant();
|
||
|
|
await using var db = _fixture.CreateContext();
|
||
|
|
var vault = Vault();
|
||
|
|
await vault.CreateAsync(db, tenant, "dupe", "httpHeaderAuth", HeaderData(), default);
|
||
|
|
|
||
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => vault.CreateAsync(
|
||
|
|
db, tenant, "dupe", "httpHeaderAuth", HeaderData("other"), default));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Update_rotates_the_secret_and_delete_removes()
|
||
|
|
{
|
||
|
|
var tenant = Tenant();
|
||
|
|
await using var db = _fixture.CreateContext();
|
||
|
|
var vault = Vault();
|
||
|
|
var created = await vault.CreateAsync(db, tenant, "rotate", "httpHeaderAuth", HeaderData("first"), default);
|
||
|
|
|
||
|
|
var updated = await vault.UpdateAsync(db, tenant, created.Id, null, null, HeaderData("second"), default);
|
||
|
|
Assert.NotNull(updated);
|
||
|
|
|
||
|
|
var resolved = await vault.ResolveAsync(
|
||
|
|
db, tenant, new Dictionary<string, string> { ["httpAuth"] = created.Id.ToString() }, default);
|
||
|
|
Assert.Equal("second", resolved["httpAuth"].Data["value"]!.GetValue<string>());
|
||
|
|
|
||
|
|
Assert.True(await vault.DeleteAsync(db, tenant, created.Id, default));
|
||
|
|
Assert.Null(await vault.GetAsync(db, tenant, created.Id, default));
|
||
|
|
}
|
||
|
|
}
|