using System.Net; using System.Net.Http.Json; using System.Text.Json; using ItWorx.KnowledgeHub.Contracts.Infrastructure; using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure; namespace ItWorx.KnowledgeHub.IntegrationTests.InfrastructureKnowledge; /// /// V2-R5 (docs/adr/ADR-029-v2-r5-final-hardening.md, brief Phase 6): the manual- /// override mechanism was already fully generic at the use-case/repository layer /// (SetManualOverrideUseCase, IInfrastructureRepository.TargetExistsAsync) /// — only the field catalog (previously an explicit, deliberately empty stub for /// every kind but Service) and a reachable endpoint were missing. Proves the field /// catalog is now real for every non-Service kind the domain models, and that the new /// generic POST /infrastructure/overrides endpoint actually works end to end /// for several of them. /// [Collection(PostgresTestGroup.Name)] public sealed class ExpandedManualOverrideCatalogTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres) { [Theory] [InlineData("Host", "hostname")] [InlineData("Component", "name")] [InlineData("Endpoint", "scheme")] [InlineData("Network", "name")] [InlineData("Volume", "name")] [InlineData("Database", "name")] [InlineData("Deployment", "repositoryUrl")] [InlineData("Backup", "method")] public async Task Every_non_service_object_kind_now_has_a_real_non_empty_override_catalog(string targetKind, string expectedField) { using var client = Factory.CreateClient(); await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword); var catalog = await client.GetFromJsonAsync( $"/api/v1/infrastructure/overrides/catalog?targetKind={targetKind}"); Assert.NotNull(catalog); Assert.NotEmpty(catalog.Fields); Assert.Contains(catalog.Fields, field => field.FieldName == expectedField); } [Fact] public async Task A_host_fact_can_be_corrected_through_the_generic_endpoint() { using var client = Factory.CreateClient(); await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword); var (hostId, _, _, _, _, _, _, _) = await SeedFullServiceGraphAsync(client); var response = await SetupHelper.PostWithCsrfAsync( client, "/api/v1/infrastructure/overrides", new SetInfrastructureOverrideRequest("Host", hostId, "hostname", JsonSerializer.SerializeToElement("gecorrigeerde-hostnaam"), "Reden.")); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Theory] [InlineData("Network", "internal")] [InlineData("Volume", "readOnly")] public async Task A_boolean_shaped_fact_is_overridable_as_a_true_false_enum(string targetKind, string fieldName) { using var client = Factory.CreateClient(); await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword); var (_, _, networkId, volumeId, _, _, _, _) = await SeedFullServiceGraphAsync(client); var targetId = targetKind == "Network" ? networkId : volumeId; var response = await SetupHelper.PostWithCsrfAsync( client, "/api/v1/infrastructure/overrides", new SetInfrastructureOverrideRequest(targetKind, targetId, fieldName, JsonSerializer.SerializeToElement("true"), "Reden.")); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var invalid = await SetupHelper.PostWithCsrfAsync( client, "/api/v1/infrastructure/overrides", new SetInfrastructureOverrideRequest(targetKind, targetId, fieldName, JsonSerializer.SerializeToElement("misschien"), "Ongeldig.")); Assert.Equal(HttpStatusCode.UnprocessableEntity, invalid.StatusCode); } [Fact] public async Task An_unknown_target_kind_string_is_rejected_not_a_crash() { using var client = Factory.CreateClient(); await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword); var (hostId, _, _, _, _, _, _, _) = await SeedFullServiceGraphAsync(client); var response = await SetupHelper.PostWithCsrfAsync( client, "/api/v1/infrastructure/overrides", new SetInfrastructureOverrideRequest("NietBestaandType", hostId, "naam", JsonSerializer.SerializeToElement("x"), null)); Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode); } [Fact] public async Task A_target_id_that_does_not_exist_for_the_given_kind_is_not_found() { using var client = Factory.CreateClient(); await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword); await SeedFullServiceGraphAsync(client); var response = await SetupHelper.PostWithCsrfAsync( client, "/api/v1/infrastructure/overrides", new SetInfrastructureOverrideRequest("Host", Guid.NewGuid(), "hostname", JsonSerializer.SerializeToElement("x"), null)); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task An_override_set_through_the_generic_endpoint_can_be_removed_through_the_existing_generic_delete() { using var client = Factory.CreateClient(); await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword); var (hostId, _, _, _, _, _, _, _) = await SeedFullServiceGraphAsync(client); var setResponse = await SetupHelper.PostWithCsrfAsync( client, "/api/v1/infrastructure/overrides", new SetInfrastructureOverrideRequest("Host", hostId, "operatingSystem", JsonSerializer.SerializeToElement("Custom OS"), "Reden.")); Assert.Equal(HttpStatusCode.OK, setResponse.StatusCode); var created = await setResponse.Content.ReadFromJsonAsync(); var overrideId = created.GetProperty("id").GetGuid(); var removeResponse = await SetupHelper.DeleteWithCsrfAsync(client, $"/api/v1/infrastructure/overrides/{overrideId}"); Assert.Equal(HttpStatusCode.NoContent, removeResponse.StatusCode); } /// Seeds one service with one of every nested fact kind via a single real /// inventory sync, then resolves each fact's real generated ID through the normal /// service-detail endpoint — never a raw SQL insert, so this also incidentally /// re-proves the sync path itself creates every fact kind correctly. private static async Task<(Guid HostId, Guid ServiceId, Guid NetworkId, Guid VolumeId, Guid DatabaseId, Guid EndpointId, Guid DeploymentId, Guid BackupId)> SeedFullServiceGraphAsync(HttpClient client) { var hash = $"hash-{Guid.NewGuid():N}"; var request = new SynchronizeInventoryRequest( $"inventory-{hash}", hash, DateTimeOffset.UtcNow.AddMinutes(-1), [new InventoryHostRequest("host:unraid", "Tower", "Unraid", "6.12", "x86_64", 16, 64L * 1024 * 1024 * 1024)], [new InventoryServiceRequest( "docker:knowledge-hub", "host:unraid", "itworx-knowledge-hub", "container", "running", "Healthy", "itworx/knowledge-hub:v2", "sha256:example", "10001:10001", true, true, false, "High", [new InventoryComponentRequest("component:app", "app", "container", "rt-1", "running", "itworx/app:v2", "High")], [new InventoryEndpointRequest("endpoint:main", "https", "wiki.example.com", 1235, 8080, "https://wiki.example.com", true, "High")], [new InventoryNetworkRequest("network:main", "knowledge-net", "bridge", "local", false, "High")], [new InventoryVolumeRequest("volume:data", "knowledge-data", "/data", "local", false, 1024, "High")], [new InventoryDatabaseRequest("database:main", "knowledge_hub", "postgresql", "18", "db", 5432, false, "High")], [], [], [new InventoryDeploymentRequest("deployment:main", "https://gitea.example.com/itworx/knowledge-hub", "abc123", "itworx/knowledge-hub:v2", "sha256:example", DateTimeOffset.UtcNow, "High")], [new InventoryBackupRequest("backup:main", "pg_dump", "daily", DateTimeOffset.UtcNow.AddDays(-1), true, "High")])]); var syncResponse = await SetupHelper.PostWithCsrfAsync(client, "/api/v1/infrastructure/inventory/snapshots", request); Assert.Equal(HttpStatusCode.OK, syncResponse.StatusCode); var overview = await client.GetFromJsonAsync("/api/v1/infrastructure/overview"); var hostId = Assert.Single(overview!.Hosts).Id; var services = await client.GetFromJsonAsync("/api/v1/infrastructure/services?includeHidden=true"); var serviceId = Assert.Single(services!.Items).Id; var detail = await client.GetFromJsonAsync($"/api/v1/infrastructure/services/{serviceId}"); Assert.NotNull(detail); return ( hostId, serviceId, Assert.Single(detail.Networks).Id, Assert.Single(detail.Volumes).Id, Assert.Single(detail.Databases).Id, Assert.Single(detail.Endpoints).Id, Assert.Single(detail.Deployments).Id, Assert.Single(detail.Backups).Id); } }