160 lines
5.8 KiB
C#
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);
|
|
}
|
|
}
|