175 lines
9.1 KiB
C#
175 lines
9.1 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// (<c>SetManualOverrideUseCase</c>, <c>IInfrastructureRepository.TargetExistsAsync</c>)
|
|
/// — 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 <c>POST /infrastructure/overrides</c> endpoint actually works end to end
|
|
/// for several of them.
|
|
/// </summary>
|
|
[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<ManualOverrideFieldCatalogResponse>(
|
|
$"/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<JsonElement>();
|
|
var overrideId = created.GetProperty("id").GetGuid();
|
|
|
|
var removeResponse = await SetupHelper.DeleteWithCsrfAsync(client, $"/api/v1/infrastructure/overrides/{overrideId}");
|
|
Assert.Equal(HttpStatusCode.NoContent, removeResponse.StatusCode);
|
|
}
|
|
|
|
/// <summary>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.</summary>
|
|
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<InfrastructureOverviewResponse>("/api/v1/infrastructure/overview");
|
|
var hostId = Assert.Single(overview!.Hosts).Id;
|
|
|
|
var services = await client.GetFromJsonAsync<InfrastructureServiceListResponse>("/api/v1/infrastructure/services?includeHidden=true");
|
|
var serviceId = Assert.Single(services!.Items).Id;
|
|
|
|
var detail = await client.GetFromJsonAsync<InfrastructureServiceDetail>($"/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);
|
|
}
|
|
}
|