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 { ["httpAuth"] = "my-api" }, default); Assert.Equal("httpHeaderAuth", resolved["httpAuth"].Type); Assert.Equal("secret-value", resolved["httpAuth"].Data["value"]!.GetValue()); } [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 { ["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(() => Vault().ResolveAsync( db, tenant, new Dictionary { ["httpAuth"] = "ghost" }, default)); } [Fact] public async Task Required_fields_are_validated_on_create() { await using var db = _fixture.CreateContext(); await Assert.ThrowsAsync(() => 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(() => 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(() => 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 { ["httpAuth"] = created.Id.ToString() }, default); Assert.Equal("second", resolved["httpAuth"].Data["value"]!.GetValue()); Assert.True(await vault.DeleteAsync(db, tenant, created.Id, default)); Assert.Null(await vault.GetAsync(db, tenant, created.Id, default)); } }