using System.Security.Cryptography; using System.Text; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using w4c_workflows.Data; using w4c_workflows.Models; using w4c_workflows.Services; using Xunit; namespace w4c_workflows.Tests; [Collection("WorkflowsPostgres")] public class WorkflowRuntimeStoreTests { private readonly WorkflowsPostgresFixture _fixture; public WorkflowRuntimeStoreTests(WorkflowsPostgresFixture fixture) { _fixture = fixture; } private static WorkflowRuntimeStore Store(WorkflowsDbContext db) => new(db, new RuntimeSelfInfoProvider(new ConfigurationBuilder().Build())); private static string Tenant() => "t" + Guid.NewGuid().ToString("N")[..12]; private static string Sha256(string raw) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(raw))).ToLowerInvariant(); [Fact] public async Task List_seeds_one_platform_runtime_per_tenant() { var tenantId = Tenant(); await using var db = _fixture.CreateContext(); var store = Store(db); var first = await store.ListAsync(tenantId); var platform = Assert.Single(first); Assert.Equal(WorkflowRuntimeKind.Platform, platform.Kind); Assert.Equal(WorkflowRuntimeStatus.Online, platform.Status); Assert.False(platform.IsDefault); // platform is the implicit fallback, not a "default" self-hosted runtime Assert.NotNull(platform.InfoJson); // Idempotent: listing again must NOT create a second platform runtime. var second = await store.ListAsync(tenantId); var platformAgain = Assert.Single(second); Assert.Equal(platform.Id, platformAgain.Id); } [Fact] public async Task Platform_runtime_is_isolated_per_tenant() { var tenantA = Tenant(); var tenantB = Tenant(); await using var db = _fixture.CreateContext(); var store = Store(db); var a = await store.ListAsync(tenantA); var b = await store.ListAsync(tenantB); var platformA = Assert.Single(a); var platformB = Assert.Single(b); Assert.NotEqual(platformA.Id, platformB.Id); // Tenant A must never see tenant B's runtimes. await store.CreateInboundAsync(tenantB, "B engine", "https://b.example.com", "secret-b"); var aAfter = await store.ListAsync(tenantA); Assert.Single(aAfter); } [Fact] public async Task CreateInboundAsync_hashes_key_and_sets_default_on_first() { var tenantId = Tenant(); await using var db = _fixture.CreateContext(); var store = Store(db); var first = await store.CreateInboundAsync(tenantId, "my-server", "https://wf.example.com/", "raw-key"); Assert.Equal(WorkflowRuntimeKind.SelfHosted, first.Kind); Assert.Equal("https://wf.example.com", first.Endpoint); // trailing slash trimmed Assert.True(first.IsDefault); Assert.NotNull(first.ApiKeyHash); Assert.NotEqual("raw-key", first.ApiKeyHash); Assert.Equal(Sha256("raw-key"), first.ApiKeyHash); // Second self-hosted runtime is not default. var second = await store.CreateInboundAsync(tenantId, "second", "https://wf2.example.com", null); Assert.False(second.IsDefault); Assert.Null(second.ApiKeyHash); } [Fact] public async Task CreateInboundAsync_rejects_invalid_endpoint() { var tenantId = Tenant(); await using var db = _fixture.CreateContext(); var store = Store(db); await Assert.ThrowsAsync( () => store.CreateInboundAsync(tenantId, "bad", "not-a-url", null)); await Assert.ThrowsAsync( () => store.CreateInboundAsync(tenantId, "bad", "ftp://example.com", null)); } [Fact] public async Task DeleteAsync_removes_self_hosted_and_reassigns_default() { var tenantId = Tenant(); await using var db = _fixture.CreateContext(); var store = Store(db); var first = await store.CreateInboundAsync(tenantId, "first", "https://a.example.com", null); var second = await store.CreateInboundAsync(tenantId, "second", "https://b.example.com", null); Assert.True(first.IsDefault); Assert.False(second.IsDefault); // Deleting the default reassigns it to the remaining self-hosted runtime. Assert.True(await store.DeleteAsync(tenantId, first.Id)); var remaining = await store.GetAsync(tenantId, second.Id); Assert.NotNull(remaining); Assert.True(remaining!.IsDefault); } [Fact] public async Task CreateInboundAsync_is_idempotent_per_endpoint() { var tenantId = Tenant(); await using var db = _fixture.CreateContext(); var store = Store(db); var first = await store.CreateInboundAsync(tenantId, "first", "https://wf.example.com/", "key"); var second = await store.CreateInboundAsync(tenantId, "second", "https://wf.example.com", "other"); // Same normalized endpoint → same row, no duplicate. Assert.Equal(first.Id, second.Id); var all = await store.ListAsync(tenantId); Assert.Equal(2, all.Count); // platform + the single self-hosted runtime Assert.Single(all, r => r.Kind == WorkflowRuntimeKind.SelfHosted); } [Fact] public async Task DeleteAsync_never_deletes_platform_runtime() { var tenantId = Tenant(); await using var db = _fixture.CreateContext(); var store = Store(db); var runtimes = await store.ListAsync(tenantId); var platform = Assert.Single(runtimes); await Assert.ThrowsAsync( () => store.DeleteAsync(tenantId, platform.Id)); // Platform is still there. var after = await store.ListAsync(tenantId); Assert.Single(after); } }