w4c-workflows-api/w4c-workflows-api.Tests/WorkflowRuntimeStoreTests.cs
2026-09-09 16:09:48 +03:00

160 lines
5.8 KiB
C#

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<ArgumentException>(
() => store.CreateInboundAsync(tenantId, "bad", "not-a-url", null));
await Assert.ThrowsAsync<ArgumentException>(
() => 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<InvalidOperationException>(
() => store.DeleteAsync(tenantId, platform.Id));
// Platform is still there.
var after = await store.ListAsync(tenantId);
Assert.Single(after);
}
}