w4c-workflows-api/w4c-workflows-api.Tests/CredentialVaultTests.cs
2026-09-12 01:02:46 +03:00

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));
}
}