Widen scratch iteration 011 with fixture-backed integrations QA
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
using static StellaOps.Localization.T;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration.Tenancy;
|
||||
using StellaOps.Integrations.Contracts;
|
||||
using StellaOps.Integrations.Contracts.AiCodeGuard;
|
||||
@@ -59,10 +61,11 @@ public static class IntegrationEndpoints
|
||||
// Get integration by ID
|
||||
group.MapGet("/{id:guid}", async (
|
||||
[FromServices] IntegrationService service,
|
||||
[FromServices] IStellaOpsTenantAccessor tenantAccessor,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await service.GetByIdAsync(id, cancellationToken);
|
||||
var result = await service.GetByIdAsync(id, tenantAccessor.TenantId, cancellationToken);
|
||||
return result is null ? Results.NotFound() : Results.Ok(result);
|
||||
})
|
||||
.RequireAuthorization(IntegrationPolicies.Read)
|
||||
@@ -73,10 +76,12 @@ public static class IntegrationEndpoints
|
||||
group.MapPost("/", async (
|
||||
[FromServices] IntegrationService service,
|
||||
[FromServices] IStellaOpsTenantAccessor tenantAccessor,
|
||||
HttpContext httpContext,
|
||||
[FromBody] CreateIntegrationRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await service.CreateAsync(request, tenantAccessor.TenantId, null, cancellationToken);
|
||||
var actorId = ResolveActorId(httpContext);
|
||||
var result = await service.CreateAsync(request, tenantAccessor.TenantId, actorId, cancellationToken);
|
||||
return Results.Created($"/api/v1/integrations/{result.Id}", result);
|
||||
})
|
||||
.RequireAuthorization(IntegrationPolicies.Write)
|
||||
@@ -87,11 +92,13 @@ public static class IntegrationEndpoints
|
||||
group.MapPut("/{id:guid}", async (
|
||||
[FromServices] IntegrationService service,
|
||||
[FromServices] IStellaOpsTenantAccessor tenantAccessor,
|
||||
HttpContext httpContext,
|
||||
Guid id,
|
||||
[FromBody] UpdateIntegrationRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await service.UpdateAsync(id, request, tenantAccessor.TenantId, cancellationToken);
|
||||
var actorId = ResolveActorId(httpContext);
|
||||
var result = await service.UpdateAsync(id, request, tenantAccessor.TenantId, actorId, cancellationToken);
|
||||
return result is null ? Results.NotFound() : Results.Ok(result);
|
||||
})
|
||||
.RequireAuthorization(IntegrationPolicies.Write)
|
||||
@@ -102,10 +109,12 @@ public static class IntegrationEndpoints
|
||||
group.MapDelete("/{id:guid}", async (
|
||||
[FromServices] IntegrationService service,
|
||||
[FromServices] IStellaOpsTenantAccessor tenantAccessor,
|
||||
HttpContext httpContext,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await service.DeleteAsync(id, tenantAccessor.TenantId, cancellationToken);
|
||||
var actorId = ResolveActorId(httpContext);
|
||||
var result = await service.DeleteAsync(id, tenantAccessor.TenantId, actorId, cancellationToken);
|
||||
return result ? Results.NoContent() : Results.NotFound();
|
||||
})
|
||||
.RequireAuthorization(IntegrationPolicies.Write)
|
||||
@@ -116,10 +125,12 @@ public static class IntegrationEndpoints
|
||||
group.MapPost("/{id:guid}/test", async (
|
||||
[FromServices] IntegrationService service,
|
||||
[FromServices] IStellaOpsTenantAccessor tenantAccessor,
|
||||
HttpContext httpContext,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await service.TestConnectionAsync(id, tenantAccessor.TenantId, cancellationToken);
|
||||
var actorId = ResolveActorId(httpContext);
|
||||
var result = await service.TestConnectionAsync(id, tenantAccessor.TenantId, actorId, cancellationToken);
|
||||
return result is null ? Results.NotFound() : Results.Ok(result);
|
||||
})
|
||||
.RequireAuthorization(IntegrationPolicies.Operate)
|
||||
@@ -129,10 +140,11 @@ public static class IntegrationEndpoints
|
||||
// Health check
|
||||
group.MapGet("/{id:guid}/health", async (
|
||||
[FromServices] IntegrationService service,
|
||||
[FromServices] IStellaOpsTenantAccessor tenantAccessor,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await service.CheckHealthAsync(id, cancellationToken);
|
||||
var result = await service.CheckHealthAsync(id, tenantAccessor.TenantId, cancellationToken);
|
||||
return result is null ? Results.NotFound() : Results.Ok(result);
|
||||
})
|
||||
.RequireAuthorization(IntegrationPolicies.Read)
|
||||
@@ -142,10 +154,11 @@ public static class IntegrationEndpoints
|
||||
// Impact map
|
||||
group.MapGet("/{id:guid}/impact", async (
|
||||
[FromServices] IntegrationService service,
|
||||
[FromServices] IStellaOpsTenantAccessor tenantAccessor,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await service.GetImpactAsync(id, cancellationToken);
|
||||
var result = await service.GetImpactAsync(id, tenantAccessor.TenantId, cancellationToken);
|
||||
return result is null ? Results.NotFound() : Results.Ok(result);
|
||||
})
|
||||
.RequireAuthorization(IntegrationPolicies.Read)
|
||||
@@ -162,4 +175,12 @@ public static class IntegrationEndpoints
|
||||
.WithName("GetSupportedProviders")
|
||||
.WithDescription(_t("integrations.integration.get_providers_description"));
|
||||
}
|
||||
|
||||
private static string? ResolveActorId(HttpContext httpContext)
|
||||
{
|
||||
return httpContext.User.FindFirst(StellaOpsClaimTypes.Subject)?.Value
|
||||
?? httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
|
||||
?? httpContext.User.FindFirst("sub")?.Value
|
||||
?? httpContext.User.Identity?.Name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class IntegrationService
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IntegrationResponse> CreateAsync(CreateIntegrationRequest request, string? userId, string? tenantId, CancellationToken cancellationToken = default)
|
||||
public async Task<IntegrationResponse> CreateAsync(CreateIntegrationRequest request, string? tenantId, string? userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var integration = new Integration
|
||||
@@ -78,9 +78,9 @@ public sealed class IntegrationService
|
||||
return MapToResponse(created);
|
||||
}
|
||||
|
||||
public async Task<IntegrationResponse?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
public async Task<IntegrationResponse?> GetByIdAsync(Guid id, string? tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var integration = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
var integration = await GetScopedIntegrationAsync(id, tenantId, cancellationToken);
|
||||
return integration is null ? null : MapToResponse(integration);
|
||||
}
|
||||
|
||||
@@ -110,9 +110,9 @@ public sealed class IntegrationService
|
||||
totalPages);
|
||||
}
|
||||
|
||||
public async Task<IntegrationResponse?> UpdateAsync(Guid id, UpdateIntegrationRequest request, string? userId, CancellationToken cancellationToken = default)
|
||||
public async Task<IntegrationResponse?> UpdateAsync(Guid id, UpdateIntegrationRequest request, string? tenantId, string? userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var integration = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
var integration = await GetScopedIntegrationAsync(id, tenantId, cancellationToken);
|
||||
if (integration is null) return null;
|
||||
|
||||
var oldStatus = integration.Status;
|
||||
@@ -153,9 +153,9 @@ public sealed class IntegrationService
|
||||
return MapToResponse(updated);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id, string? userId, CancellationToken cancellationToken = default)
|
||||
public async Task<bool> DeleteAsync(Guid id, string? tenantId, string? userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var integration = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
var integration = await GetScopedIntegrationAsync(id, tenantId, cancellationToken);
|
||||
if (integration is null) return false;
|
||||
|
||||
await _repository.DeleteAsync(id, cancellationToken);
|
||||
@@ -172,9 +172,9 @@ public sealed class IntegrationService
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<TestConnectionResponse?> TestConnectionAsync(Guid id, string? userId, CancellationToken cancellationToken = default)
|
||||
public async Task<TestConnectionResponse?> TestConnectionAsync(Guid id, string? tenantId, string? userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var integration = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
var integration = await GetScopedIntegrationAsync(id, tenantId, cancellationToken);
|
||||
if (integration is null) return null;
|
||||
|
||||
var plugin = _pluginLoader.GetByProvider(integration.Provider);
|
||||
@@ -227,9 +227,9 @@ public sealed class IntegrationService
|
||||
endTime);
|
||||
}
|
||||
|
||||
public async Task<HealthCheckResponse?> CheckHealthAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
public async Task<HealthCheckResponse?> CheckHealthAsync(Guid id, string? tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var integration = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
var integration = await GetScopedIntegrationAsync(id, tenantId, cancellationToken);
|
||||
if (integration is null) return null;
|
||||
|
||||
var plugin = _pluginLoader.GetByProvider(integration.Provider);
|
||||
@@ -269,9 +269,9 @@ public sealed class IntegrationService
|
||||
result.Duration);
|
||||
}
|
||||
|
||||
public async Task<IntegrationImpactResponse?> GetImpactAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
public async Task<IntegrationImpactResponse?> GetImpactAsync(Guid id, string? tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var integration = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
var integration = await GetScopedIntegrationAsync(id, tenantId, cancellationToken);
|
||||
if (integration is null)
|
||||
{
|
||||
return null;
|
||||
@@ -302,6 +302,27 @@ public sealed class IntegrationService
|
||||
p.Provider)).ToList();
|
||||
}
|
||||
|
||||
private async Task<Integration?> GetScopedIntegrationAsync(Guid id, string? tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var integration = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
if (integration is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.Equals(integration.TenantId, tenantId, StringComparison.Ordinal))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Integration {IntegrationId} was requested outside its tenant scope. requestedTenant={RequestedTenant} actualTenant={ActualTenant}",
|
||||
id,
|
||||
tenantId,
|
||||
integration.TenantId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return integration;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ImpactedWorkflow> BuildImpactedWorkflows(Integration integration)
|
||||
{
|
||||
var blockedByStatus = integration.Status is IntegrationStatus.Failed or IntegrationStatus.Disabled or IntegrationStatus.Archived;
|
||||
|
||||
@@ -41,7 +41,7 @@ public sealed class GitHubAppConnectorPlugin : IIntegrationConnectorPlugin
|
||||
try
|
||||
{
|
||||
// Call GitHub API to verify authentication
|
||||
var response = await client.GetAsync("/app", cancellationToken);
|
||||
var response = await client.GetAsync("app", cancellationToken);
|
||||
var duration = _timeProvider.GetUtcNow() - startTime;
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
@@ -98,7 +98,7 @@ public sealed class GitHubAppConnectorPlugin : IIntegrationConnectorPlugin
|
||||
try
|
||||
{
|
||||
// Check GitHub API status
|
||||
var response = await client.GetAsync("/rate_limit", cancellationToken);
|
||||
var response = await client.GetAsync("rate_limit", cancellationToken);
|
||||
var duration = _timeProvider.GetUtcNow() - startTime;
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
@@ -151,9 +151,7 @@ public sealed class GitHubAppConnectorPlugin : IIntegrationConnectorPlugin
|
||||
|
||||
private static HttpClient CreateHttpClient(IntegrationConfig config)
|
||||
{
|
||||
var baseUrl = string.IsNullOrEmpty(config.Endpoint) || config.Endpoint == "https://github.com"
|
||||
? "https://api.github.com"
|
||||
: config.Endpoint.TrimEnd('/') + "/api/v3";
|
||||
var baseUrl = ResolveBaseUrl(config.Endpoint);
|
||||
|
||||
var client = new HttpClient
|
||||
{
|
||||
@@ -174,6 +172,22 @@ public sealed class GitHubAppConnectorPlugin : IIntegrationConnectorPlugin
|
||||
return client;
|
||||
}
|
||||
|
||||
private static string ResolveBaseUrl(string? endpoint)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(endpoint) || endpoint.Equals("https://github.com", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "https://api.github.com/";
|
||||
}
|
||||
|
||||
var normalized = endpoint.TrimEnd('/');
|
||||
if (normalized.EndsWith("/api/v3", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return normalized + "/";
|
||||
}
|
||||
|
||||
return normalized + "/api/v3/";
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using StellaOps.Integrations.Core;
|
||||
using StellaOps.Integrations.Plugin.GitHubApp;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Integrations.Plugin.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Focused transport-level tests for GitHubAppConnectorPlugin route construction.
|
||||
/// </summary>
|
||||
public sealed class GitHubAppConnectorPluginTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedTime = new(2026, 3, 14, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public async Task TestConnectionAsync_UsesApiV3AppRoute_ForEnterpriseBaseEndpoint()
|
||||
{
|
||||
using var fixture = LoopbackHttpFixture.Start(path => path switch
|
||||
{
|
||||
"/api/v3/app" => HttpResponse.Json("""{"id":424242,"name":"Stella QA GitHub App","slug":"stella-qa-app"}"""),
|
||||
_ => HttpResponse.Text("unexpected-path"),
|
||||
});
|
||||
|
||||
var plugin = new GitHubAppConnectorPlugin(new FixedTimeProvider(FixedTime));
|
||||
|
||||
var result = await plugin.TestConnectionAsync(CreateConfig(fixture.BaseUrl));
|
||||
var requestedPath = await fixture.WaitForPathAsync();
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("/api/v3/app", requestedPath);
|
||||
Assert.Contains("Stella QA GitHub App", result.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestConnectionAsync_DoesNotDuplicateApiV3_WhenEndpointAlreadyIncludesIt()
|
||||
{
|
||||
using var fixture = LoopbackHttpFixture.Start(path => path switch
|
||||
{
|
||||
"/api/v3/app" => HttpResponse.Json("""{"id":424242,"name":"Stella QA GitHub App","slug":"stella-qa-app"}"""),
|
||||
_ => HttpResponse.Text("unexpected-path"),
|
||||
});
|
||||
|
||||
var plugin = new GitHubAppConnectorPlugin(new FixedTimeProvider(FixedTime));
|
||||
|
||||
var result = await plugin.TestConnectionAsync(CreateConfig($"{fixture.BaseUrl}/api/v3"));
|
||||
var requestedPath = await fixture.WaitForPathAsync();
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("/api/v3/app", requestedPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_UsesApiV3RateLimitRoute_ForEnterpriseBaseEndpoint()
|
||||
{
|
||||
using var fixture = LoopbackHttpFixture.Start(path => path switch
|
||||
{
|
||||
"/api/v3/rate_limit" => HttpResponse.Json("""{"resources":{"core":{"limit":5000,"remaining":4991,"reset":1893456000}}}"""),
|
||||
_ => HttpResponse.Text("unexpected-path"),
|
||||
});
|
||||
|
||||
var plugin = new GitHubAppConnectorPlugin(new FixedTimeProvider(FixedTime));
|
||||
|
||||
var result = await plugin.CheckHealthAsync(CreateConfig(fixture.BaseUrl));
|
||||
var requestedPath = await fixture.WaitForPathAsync();
|
||||
|
||||
Assert.Equal(HealthStatus.Healthy, result.Status);
|
||||
Assert.Equal("/api/v3/rate_limit", requestedPath);
|
||||
Assert.Contains("Rate limit", result.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static IntegrationConfig CreateConfig(string endpoint)
|
||||
{
|
||||
return new IntegrationConfig(
|
||||
IntegrationId: Guid.NewGuid(),
|
||||
Type: IntegrationType.Scm,
|
||||
Provider: IntegrationProvider.GitHubApp,
|
||||
Endpoint: endpoint,
|
||||
ResolvedSecret: null,
|
||||
OrganizationId: "stellaops",
|
||||
ExtendedConfig: new Dictionary<string, object>
|
||||
{
|
||||
["appId"] = "424242",
|
||||
["installationId"] = "424243",
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _fixedTime;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset fixedTime) => _fixedTime = fixedTime;
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _fixedTime;
|
||||
}
|
||||
|
||||
private sealed class LoopbackHttpFixture : IDisposable
|
||||
{
|
||||
private readonly TcpListener _listener;
|
||||
private readonly Task<string> _requestPathTask;
|
||||
|
||||
private LoopbackHttpFixture(Func<string, HttpResponse> responder)
|
||||
{
|
||||
_listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
_listener.Start();
|
||||
BaseUrl = $"http://127.0.0.1:{((IPEndPoint)_listener.LocalEndpoint).Port}";
|
||||
_requestPathTask = HandleSingleRequestAsync(responder);
|
||||
}
|
||||
|
||||
public string BaseUrl { get; }
|
||||
|
||||
public static LoopbackHttpFixture Start(Func<string, HttpResponse> responder) => new(responder);
|
||||
|
||||
public Task<string> WaitForPathAsync() => _requestPathTask;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
_listener.Stop();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> HandleSingleRequestAsync(Func<string, HttpResponse> responder)
|
||||
{
|
||||
using var client = await _listener.AcceptTcpClientAsync();
|
||||
using var stream = client.GetStream();
|
||||
using var reader = new StreamReader(
|
||||
stream,
|
||||
Encoding.ASCII,
|
||||
detectEncodingFromByteOrderMarks: false,
|
||||
bufferSize: 1024,
|
||||
leaveOpen: true);
|
||||
|
||||
var requestLine = await reader.ReadLineAsync();
|
||||
if (string.IsNullOrWhiteSpace(requestLine))
|
||||
{
|
||||
throw new InvalidOperationException("Did not receive an HTTP request line.");
|
||||
}
|
||||
|
||||
var requestParts = requestLine.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (requestParts.Length < 2)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected HTTP request line: {requestLine}");
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
var headerLine = await reader.ReadLineAsync();
|
||||
if (string.IsNullOrEmpty(headerLine))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var requestPath = requestParts[1];
|
||||
var response = responder(requestPath);
|
||||
var payload = Encoding.UTF8.GetBytes(response.Body);
|
||||
var responseText =
|
||||
$"HTTP/1.1 {response.StatusCode} {response.ReasonPhrase}\r\n" +
|
||||
$"Content-Type: {response.ContentType}\r\n" +
|
||||
$"Content-Length: {payload.Length}\r\n" +
|
||||
"Connection: close\r\n" +
|
||||
"\r\n";
|
||||
|
||||
var headerBytes = Encoding.ASCII.GetBytes(responseText);
|
||||
await stream.WriteAsync(headerBytes);
|
||||
await stream.WriteAsync(payload);
|
||||
await stream.FlushAsync();
|
||||
return requestPath;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record HttpResponse(int StatusCode, string ReasonPhrase, string ContentType, string Body)
|
||||
{
|
||||
public static HttpResponse Json(string body) => new(200, "OK", "application/json", body);
|
||||
|
||||
public static HttpResponse Text(string body) => new(200, "OK", "text/plain", body);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Plugins\StellaOps.Integrations.Plugin.InMemory\StellaOps.Integrations.Plugin.InMemory.csproj" />
|
||||
<ProjectReference Include="..\..\__Plugins\StellaOps.Integrations.Plugin.GitHubApp\StellaOps.Integrations.Plugin.GitHubApp.csproj" />
|
||||
<ProjectReference Include="..\..\__Plugins\StellaOps.Integrations.Plugin.Harbor\StellaOps.Integrations.Plugin.Harbor.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Integrations.Persistence\StellaOps.Integrations.Persistence.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -30,6 +30,42 @@ public sealed class IntegrationImpactEndpointsTests : IClassFixture<IntegrationI
|
||||
_client = factory.CreateClient();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Integration)]
|
||||
[Fact]
|
||||
public async Task CreateThenListEndpoint_ReturnsCreatedItemForCurrentTenant()
|
||||
{
|
||||
var createRequest = new CreateIntegrationRequest(
|
||||
Name: $"Tenant Harbor {Guid.NewGuid():N}",
|
||||
Description: "Registry integration",
|
||||
Type: IntegrationType.Registry,
|
||||
Provider: IntegrationProvider.InMemory,
|
||||
Endpoint: "http://inmemory.local",
|
||||
AuthRefUri: "authref://vault/inmemory#token",
|
||||
OrganizationId: "tenant-scope",
|
||||
ExtendedConfig: new Dictionary<string, object> { ["source"] = "integration-test" },
|
||||
Tags: ["qa", "tenant"]);
|
||||
|
||||
var createResponse = await _client.PostAsJsonAsync(
|
||||
"/api/v1/integrations/",
|
||||
createRequest,
|
||||
TestContext.Current.CancellationToken);
|
||||
createResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var created = await createResponse.Content.ReadFromJsonAsync<IntegrationResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(created);
|
||||
Assert.Equal("test-user", created!.CreatedBy);
|
||||
|
||||
var list = await _client.GetFromJsonAsync<PagedIntegrationsResponse>(
|
||||
$"/api/v1/integrations/?type={(int)IntegrationType.Registry}",
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(list);
|
||||
Assert.Equal(1, list!.TotalCount);
|
||||
var item = Assert.Single(list.Items);
|
||||
Assert.Equal(created.Id, item.Id);
|
||||
Assert.Equal("test-user", item.CreatedBy);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Integration)]
|
||||
[Fact]
|
||||
public async Task ImpactEndpoint_ReturnsDeterministicWorkflowMap()
|
||||
@@ -220,7 +256,41 @@ internal sealed class InMemoryIntegrationRepository : IIntegrationRepository
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return Task.FromResult(_items.Values.Count(item => query.IncludeDeleted || !item.IsDeleted));
|
||||
IEnumerable<Integration> values = _items.Values;
|
||||
|
||||
if (!query.IncludeDeleted)
|
||||
{
|
||||
values = values.Where(item => !item.IsDeleted);
|
||||
}
|
||||
|
||||
if (query.Type.HasValue)
|
||||
{
|
||||
values = values.Where(item => item.Type == query.Type.Value);
|
||||
}
|
||||
|
||||
if (query.Provider.HasValue)
|
||||
{
|
||||
values = values.Where(item => item.Provider == query.Provider.Value);
|
||||
}
|
||||
|
||||
if (query.Status.HasValue)
|
||||
{
|
||||
values = values.Where(item => item.Status == query.Status.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.TenantId))
|
||||
{
|
||||
values = values.Where(item => string.Equals(item.TenantId, query.TenantId, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.Search))
|
||||
{
|
||||
values = values.Where(item =>
|
||||
item.Name.Contains(query.Search, StringComparison.OrdinalIgnoreCase) ||
|
||||
(item.Description?.Contains(query.Search, StringComparison.OrdinalIgnoreCase) ?? false));
|
||||
}
|
||||
|
||||
return Task.FromResult(values.Count());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ using Xunit;
|
||||
|
||||
namespace StellaOps.Integrations.Tests;
|
||||
|
||||
public class IntegrationServiceTests
|
||||
public sealed class IntegrationServiceTests
|
||||
{
|
||||
private readonly Mock<IIntegrationRepository> _repositoryMock;
|
||||
private readonly Mock<IIntegrationEventPublisher> _eventPublisherMock;
|
||||
@@ -38,9 +38,8 @@ public class IntegrationServiceTests
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task CreateAsync_WithValidRequest_CreatesIntegration()
|
||||
public async Task CreateAsync_WithValidRequest_PersistsTenantAndActor()
|
||||
{
|
||||
// Arrange
|
||||
var request = new CreateIntegrationRequest(
|
||||
Name: "Test Registry",
|
||||
Description: "Test description",
|
||||
@@ -48,55 +47,43 @@ public class IntegrationServiceTests
|
||||
Provider: IntegrationProvider.Harbor,
|
||||
Endpoint: "https://harbor.example.com",
|
||||
AuthRefUri: "authref://vault/harbor#credentials",
|
||||
OrganizationId: "myorg",
|
||||
OrganizationId: "platform",
|
||||
ExtendedConfig: null,
|
||||
Tags: ["test", "dev"]);
|
||||
|
||||
_repositoryMock
|
||||
.Setup(r => r.CreateAsync(It.IsAny<Integration>(), It.IsAny<CancellationToken>()))
|
||||
.Returns<Integration, CancellationToken>((i, _) => Task.FromResult(i));
|
||||
.Returns<Integration, CancellationToken>((integration, _) => Task.FromResult(integration));
|
||||
|
||||
// Act
|
||||
var result = await _service.CreateAsync(request, "test-user", "tenant-1");
|
||||
var result = await _service.CreateAsync(request, "tenant-1", "test-user");
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Name.Should().Be("Test Registry");
|
||||
result.Type.Should().Be(IntegrationType.Registry);
|
||||
result.Provider.Should().Be(IntegrationProvider.Harbor);
|
||||
result.Status.Should().Be(IntegrationStatus.Pending);
|
||||
result.Endpoint.Should().Be("https://harbor.example.com");
|
||||
result.CreatedBy.Should().Be("test-user");
|
||||
result.UpdatedBy.Should().Be("test-user");
|
||||
|
||||
_repositoryMock.Verify(r => r.CreateAsync(
|
||||
It.IsAny<Integration>(),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
_eventPublisherMock.Verify(e => e.PublishAsync(
|
||||
It.IsAny<IntegrationCreatedEvent>(),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
_auditLoggerMock.Verify(a => a.LogAsync(
|
||||
"integration.created",
|
||||
It.IsAny<Guid>(),
|
||||
"test-user",
|
||||
It.IsAny<object>(),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
_repositoryMock.Verify(
|
||||
r => r.CreateAsync(
|
||||
It.Is<Integration>(integration =>
|
||||
integration.TenantId == "tenant-1" &&
|
||||
integration.CreatedBy == "test-user" &&
|
||||
integration.UpdatedBy == "test-user"),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_WithExistingId_ReturnsIntegration()
|
||||
public async Task GetByIdAsync_WithMatchingTenant_ReturnsIntegration()
|
||||
{
|
||||
// Arrange
|
||||
var integration = CreateTestIntegration();
|
||||
_repositoryMock
|
||||
.Setup(r => r.GetByIdAsync(integration.Id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(integration);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetByIdAsync(integration.Id);
|
||||
var result = await _service.GetByIdAsync(integration.Id, "tenant-1");
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Id.Should().Be(integration.Id);
|
||||
result.Name.Should().Be(integration.Name);
|
||||
@@ -104,61 +91,56 @@ public class IntegrationServiceTests
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_WithNonExistingId_ReturnsNull()
|
||||
public async Task GetByIdAsync_WithTenantMismatch_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var id = Guid.NewGuid();
|
||||
var integration = CreateTestIntegration(tenantId: "tenant-a");
|
||||
_repositoryMock
|
||||
.Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((Integration?)null);
|
||||
.Setup(r => r.GetByIdAsync(integration.Id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(integration);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetByIdAsync(id);
|
||||
var result = await _service.GetByIdAsync(integration.Id, "tenant-b");
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task ListAsync_WithFilters_ReturnsFilteredResults()
|
||||
public async Task ListAsync_WithFilters_ScopesRepositoryQueryToTenant()
|
||||
{
|
||||
// Arrange
|
||||
var integrations = new[]
|
||||
{
|
||||
CreateTestIntegration(type: IntegrationType.Registry),
|
||||
CreateTestIntegration(type: IntegrationType.Registry),
|
||||
CreateTestIntegration(type: IntegrationType.Scm)
|
||||
CreateTestIntegration(type: IntegrationType.Scm),
|
||||
};
|
||||
|
||||
_repositoryMock
|
||||
.Setup(r => r.GetAllAsync(
|
||||
It.Is<IntegrationQuery>(q => q.Type == IntegrationType.Registry),
|
||||
It.Is<IntegrationQuery>(query =>
|
||||
query.Type == IntegrationType.Registry &&
|
||||
query.TenantId == "tenant-1"),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(integrations.Where(i => i.Type == IntegrationType.Registry).ToList());
|
||||
|
||||
_repositoryMock
|
||||
.Setup(r => r.CountAsync(
|
||||
It.Is<IntegrationQuery>(q => q.Type == IntegrationType.Registry),
|
||||
It.Is<IntegrationQuery>(query =>
|
||||
query.Type == IntegrationType.Registry &&
|
||||
query.TenantId == "tenant-1"),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(2);
|
||||
|
||||
var query = new ListIntegrationsQuery(Type: IntegrationType.Registry);
|
||||
var result = await _service.ListAsync(new ListIntegrationsQuery(Type: IntegrationType.Registry), "tenant-1");
|
||||
|
||||
// Act
|
||||
var result = await _service.ListAsync(query, "tenant-1");
|
||||
|
||||
// Assert
|
||||
result.Items.Should().HaveCount(2);
|
||||
result.Items.Should().OnlyContain(i => i.Type == IntegrationType.Registry);
|
||||
result.Items.Should().OnlyContain(item => item.Type == IntegrationType.Registry);
|
||||
result.TotalCount.Should().Be(2);
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task UpdateAsync_WithExistingIntegration_UpdatesAndPublishesEvent()
|
||||
public async Task UpdateAsync_WithMatchingTenant_UpdatesAndPublishesEvent()
|
||||
{
|
||||
// Arrange
|
||||
var integration = CreateTestIntegration();
|
||||
var request = new UpdateIntegrationRequest(
|
||||
Name: "Updated Name",
|
||||
@@ -175,49 +157,49 @@ public class IntegrationServiceTests
|
||||
.ReturnsAsync(integration);
|
||||
_repositoryMock
|
||||
.Setup(r => r.UpdateAsync(It.IsAny<Integration>(), It.IsAny<CancellationToken>()))
|
||||
.Returns<Integration, CancellationToken>((i, _) => Task.FromResult(i));
|
||||
.Returns<Integration, CancellationToken>((updated, _) => Task.FromResult(updated));
|
||||
|
||||
// Act
|
||||
var result = await _service.UpdateAsync(integration.Id, request, "test-user");
|
||||
var result = await _service.UpdateAsync(integration.Id, request, "tenant-1", "test-user");
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Name.Should().Be("Updated Name");
|
||||
result.Description.Should().Be("Updated description");
|
||||
result.Endpoint.Should().Be("https://updated.example.com");
|
||||
|
||||
_eventPublisherMock.Verify(e => e.PublishAsync(
|
||||
It.IsAny<IntegrationUpdatedEvent>(),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
_eventPublisherMock.Verify(
|
||||
publisher => publisher.PublishAsync(It.IsAny<IntegrationUpdatedEvent>(), It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task UpdateAsync_WithNonExistingIntegration_ReturnsNull()
|
||||
public async Task UpdateAsync_WithTenantMismatch_ReturnsNullWithoutMutation()
|
||||
{
|
||||
// Arrange
|
||||
var id = Guid.NewGuid();
|
||||
_repositoryMock
|
||||
.Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((Integration?)null);
|
||||
|
||||
var integration = CreateTestIntegration(tenantId: "tenant-a");
|
||||
var request = new UpdateIntegrationRequest(
|
||||
Name: "Updated", Description: null, Endpoint: null,
|
||||
AuthRefUri: null, OrganizationId: null, ExtendedConfig: null,
|
||||
Tags: null, Status: null);
|
||||
Name: "Updated",
|
||||
Description: null,
|
||||
Endpoint: null,
|
||||
AuthRefUri: null,
|
||||
OrganizationId: null,
|
||||
ExtendedConfig: null,
|
||||
Tags: null,
|
||||
Status: null);
|
||||
|
||||
// Act
|
||||
var result = await _service.UpdateAsync(id, request, "test-user");
|
||||
_repositoryMock
|
||||
.Setup(r => r.GetByIdAsync(integration.Id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(integration);
|
||||
|
||||
var result = await _service.UpdateAsync(integration.Id, request, "tenant-b", "test-user");
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
_repositoryMock.Verify(r => r.UpdateAsync(It.IsAny<Integration>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task DeleteAsync_WithExistingIntegration_DeletesAndPublishesEvent()
|
||||
public async Task DeleteAsync_WithMatchingTenant_DeletesAndPublishesEvent()
|
||||
{
|
||||
// Arrange
|
||||
var integration = CreateTestIntegration();
|
||||
_repositoryMock
|
||||
.Setup(r => r.GetByIdAsync(integration.Id, It.IsAny<CancellationToken>()))
|
||||
@@ -226,51 +208,38 @@ public class IntegrationServiceTests
|
||||
.Setup(r => r.DeleteAsync(integration.Id, It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
var result = await _service.DeleteAsync(integration.Id, "test-user");
|
||||
var result = await _service.DeleteAsync(integration.Id, "tenant-1", "test-user");
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
_repositoryMock.Verify(r => r.DeleteAsync(integration.Id, It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
_eventPublisherMock.Verify(e => e.PublishAsync(
|
||||
It.IsAny<IntegrationDeletedEvent>(),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task DeleteAsync_WithNonExistingIntegration_ReturnsFalse()
|
||||
public async Task DeleteAsync_WithTenantMismatch_ReturnsFalseWithoutDelete()
|
||||
{
|
||||
// Arrange
|
||||
var id = Guid.NewGuid();
|
||||
var integration = CreateTestIntegration(tenantId: "tenant-a");
|
||||
_repositoryMock
|
||||
.Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((Integration?)null);
|
||||
.Setup(r => r.GetByIdAsync(integration.Id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(integration);
|
||||
|
||||
// Act
|
||||
var result = await _service.DeleteAsync(id, "test-user");
|
||||
var result = await _service.DeleteAsync(integration.Id, "tenant-b", "test-user");
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
_repositoryMock.Verify(r => r.DeleteAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task TestConnectionAsync_WithNoPlugin_ReturnsFailureResult()
|
||||
public async Task TestConnectionAsync_WithNoPlugin_ReturnsFailureResultForMatchingTenant()
|
||||
{
|
||||
// Arrange
|
||||
var integration = CreateTestIntegration(provider: IntegrationProvider.Harbor);
|
||||
_repositoryMock
|
||||
.Setup(r => r.GetByIdAsync(integration.Id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(integration);
|
||||
|
||||
// No plugins loaded in _pluginLoader
|
||||
var result = await _service.TestConnectionAsync(integration.Id, "tenant-1", "test-user");
|
||||
|
||||
// Act
|
||||
var result = await _service.TestConnectionAsync(integration.Id, "test-user");
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Success.Should().BeFalse();
|
||||
result.Message.Should().Contain("No connector plugin");
|
||||
@@ -278,66 +247,65 @@ public class IntegrationServiceTests
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task TestConnectionAsync_WithNonExistingIntegration_ReturnsNull()
|
||||
public async Task TestConnectionAsync_WithTenantMismatch_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var id = Guid.NewGuid();
|
||||
var integration = CreateTestIntegration(tenantId: "tenant-a");
|
||||
_repositoryMock
|
||||
.Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((Integration?)null);
|
||||
.Setup(r => r.GetByIdAsync(integration.Id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(integration);
|
||||
|
||||
// Act
|
||||
var result = await _service.TestConnectionAsync(id, "test-user");
|
||||
var result = await _service.TestConnectionAsync(integration.Id, "tenant-b", "test-user");
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_WithNoPlugin_ReturnsUnknownStatus()
|
||||
public async Task CheckHealthAsync_WithNoPlugin_ReturnsUnknownStatusForMatchingTenant()
|
||||
{
|
||||
// Arrange
|
||||
var integration = CreateTestIntegration();
|
||||
_repositoryMock
|
||||
.Setup(r => r.GetByIdAsync(integration.Id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(integration);
|
||||
|
||||
// No plugins loaded
|
||||
var result = await _service.CheckHealthAsync(integration.Id, "tenant-1");
|
||||
|
||||
// Act
|
||||
var result = await _service.CheckHealthAsync(integration.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Status.Should().Be(HealthStatus.Unknown);
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public void GetSupportedProviders_WithNoPlugins_ReturnsEmpty()
|
||||
public async Task CheckHealthAsync_WithTenantMismatch_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var result = _service.GetSupportedProviders();
|
||||
var integration = CreateTestIntegration(tenantId: "tenant-a");
|
||||
_repositoryMock
|
||||
.Setup(r => r.GetByIdAsync(integration.Id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(integration);
|
||||
|
||||
// Assert
|
||||
result.Should().BeEmpty();
|
||||
var result = await _service.CheckHealthAsync(integration.Id, "tenant-b");
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task GetImpactAsync_WithNonExistingIntegration_ReturnsNull()
|
||||
public void GetSupportedProviders_WithNoPlugins_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var id = Guid.NewGuid();
|
||||
_service.GetSupportedProviders().Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task GetImpactAsync_WithTenantMismatch_ReturnsNull()
|
||||
{
|
||||
var integration = CreateTestIntegration(tenantId: "tenant-a");
|
||||
_repositoryMock
|
||||
.Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((Integration?)null);
|
||||
.Setup(r => r.GetByIdAsync(integration.Id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(integration);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetImpactAsync(id);
|
||||
var result = await _service.GetImpactAsync(integration.Id, "tenant-b");
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
@@ -345,7 +313,6 @@ public class IntegrationServiceTests
|
||||
[Fact]
|
||||
public async Task GetImpactAsync_WithFailedFeedMirror_ReturnsBlockingHighSeverity()
|
||||
{
|
||||
// Arrange
|
||||
var integration = CreateTestIntegration(
|
||||
type: IntegrationType.FeedMirror,
|
||||
provider: IntegrationProvider.NvdMirror);
|
||||
@@ -356,10 +323,8 @@ public class IntegrationServiceTests
|
||||
.Setup(r => r.GetByIdAsync(integration.Id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(integration);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetImpactAsync(integration.Id);
|
||||
var result = await _service.GetImpactAsync(integration.Id, "tenant-1");
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Severity.Should().Be("high");
|
||||
result.BlockingWorkflowCount.Should().Be(result.TotalWorkflowCount);
|
||||
@@ -370,7 +335,8 @@ public class IntegrationServiceTests
|
||||
|
||||
private static Integration CreateTestIntegration(
|
||||
IntegrationType type = IntegrationType.Registry,
|
||||
IntegrationProvider provider = IntegrationProvider.Harbor)
|
||||
IntegrationProvider provider = IntegrationProvider.Harbor,
|
||||
string tenantId = "tenant-1")
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
return new Integration
|
||||
@@ -382,10 +348,12 @@ public class IntegrationServiceTests
|
||||
Status = IntegrationStatus.Active,
|
||||
Endpoint = "https://example.com",
|
||||
Description = "Test description",
|
||||
TenantId = tenantId,
|
||||
Tags = ["test"],
|
||||
CreatedBy = "test-user",
|
||||
UpdatedBy = "test-user",
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
UpdatedAt = now,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user