using System; using System.Net; using System.Collections.Generic; using System.Net.Http.Json; using System.Threading.Tasks; using FluentAssertions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using StellaOps.Concelier.Storage; using StellaOps.Concelier.Core.Jobs; using StellaOps.Concelier.Core.Orchestration; using StellaOps.Concelier.WebService; using StellaOps.Concelier.WebService.Options; using Xunit; using StellaOps.TestKit; namespace StellaOps.Concelier.WebService.Tests; public sealed class OrchestratorTestWebAppFactory : WebApplicationFactory { public OrchestratorTestWebAppFactory() { Environment.SetEnvironmentVariable("CONCELIER__POSTGRESSTORAGE__CONNECTIONSTRING", "Host=localhost;Port=5432;Database=orch-tests"); Environment.SetEnvironmentVariable("CONCELIER__POSTGRESSTORAGE__COMMANDTIMEOUTSECONDS", "30"); Environment.SetEnvironmentVariable("CONCELIER__TELEMETRY__ENABLED", "false"); Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ENABLED", "false"); // disable auth so tests can hit endpoints without tokens Environment.SetEnvironmentVariable("CONCELIER_SKIP_OPTIONS_VALIDATION", "1"); Environment.SetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN", "Host=localhost;Port=5432;Database=orch-tests"); Environment.SetEnvironmentVariable("CONCELIER_BYPASS_EXTERNAL_STORAGE", "1"); Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Testing"); Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Testing"); } protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment("Testing"); builder.ConfigureAppConfiguration(cfg => { cfg.AddInMemoryCollection(new Dictionary { ["Concelier:PostgresStorage:ConnectionString"] = "Host=localhost;Port=5432;Database=orch-tests", ["Concelier:PostgresStorage:CommandTimeoutSeconds"] = "30", ["Concelier:Telemetry:Enabled"] = "false", ["Concelier:Authority:Enabled"] = "false" }); }); builder.ConfigureServices(services => { services.RemoveAll(); services.AddSingleton(); services.RemoveAll(); services.AddSingleton(); // Pre-bind options to keep Program from trying to rebind/validate during tests. services.RemoveAll(); services.RemoveAll>(); var forcedOptions = new ConcelierOptions { PostgresStorage = new ConcelierOptions.PostgresStorageOptions { ConnectionString = "Host=localhost;Port=5432;Database=orch-tests", CommandTimeoutSeconds = 30 }, Telemetry = new ConcelierOptions.TelemetryOptions { Enabled = false }, Authority = new ConcelierOptions.AuthorityOptions { Enabled = false } }; services.AddSingleton(forcedOptions); services.AddSingleton>(_ => Microsoft.Extensions.Options.Options.Create(forcedOptions)); // Force storage options to a deterministic in-memory test DSN. services.PostConfigure(opts => { opts.ConnectionString = "Host=localhost;Port=5432;Database=orch-tests"; opts.DatabaseName = "orch-tests"; opts.CommandTimeout = TimeSpan.FromSeconds(30); }); }); } } public sealed class OrchestratorEndpointsTests : IClassFixture { private readonly OrchestratorTestWebAppFactory _factory; public OrchestratorEndpointsTests(OrchestratorTestWebAppFactory factory) => _factory = factory; [Trait("Category", TestCategories.Unit)] [Fact] public async Task Registry_accepts_valid_request_with_tenant() { var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-a"); var request = new { connectorId = "demo-connector", source = "demo-source", capabilities = new[] { "ingest" }, authRef = "secret", schedule = new { cron = "0 0 * * *", timeZone = "UTC", maxParallelRuns = 1, maxLagMinutes = 5 }, ratePolicy = new { rpm = 100, burst = 10, cooldownSeconds = 5 }, artifactKinds = new[] { "advisory" }, lockKey = "lk-1", egressGuard = new { allowlist = new[] { "example.com" }, airgapMode = false } }; var response = await client.PostAsJsonAsync("/internal/orch/registry", request); response.StatusCode.Should().Be(HttpStatusCode.Accepted); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task Heartbeat_accepts_valid_request_with_tenant() { var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-a"); var request = new { connectorId = "demo-connector", runId = "11111111-1111-1111-1111-111111111111", sequence = 1, status = "running", progress = 10, queueDepth = 0 }; var response = await client.PostAsJsonAsync("/internal/orch/heartbeat", request); response.StatusCode.Should().Be(HttpStatusCode.Accepted); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task Commands_get_returns_ok_with_empty_list() { var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-a"); var response = await client.GetAsync("/internal/orch/commands?connectorId=demo-connector&runId=11111111-1111-1111-1111-111111111111"); response.StatusCode.Should().Be(HttpStatusCode.OK); var payload = await response.Content.ReadFromJsonAsync>(); payload.Should().NotBeNull(); payload!.Should().BeEmpty(); } }