search and ai stabilization work, localization stablized.
This commit is contained in:
@@ -4567,6 +4567,7 @@ spec:
|
||||
public EntryTraceResponseModel? EntryTraceResponse { get; set; }
|
||||
public Exception? EntryTraceException { get; set; }
|
||||
public string? LastEntryTraceScanId { get; private set; }
|
||||
public (string Tenant, string Locale)? LastLanguagePreferenceSet { get; private set; }
|
||||
public List<(AdvisoryAiTaskType TaskType, AdvisoryPipelinePlanRequestModel Request)> AdvisoryPlanRequests { get; } = new();
|
||||
public AdvisoryPipelinePlanResponseModel? AdvisoryPlanResponse { get; set; }
|
||||
public Exception? AdvisoryPlanException { get; set; }
|
||||
@@ -4947,6 +4948,20 @@ spec:
|
||||
public Task<AnalyticsListResponse<AnalyticsComponentTrendPoint>> GetAnalyticsComponentTrendsAsync(string? environment, int? days, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(new AnalyticsListResponse<AnalyticsComponentTrendPoint>(Array.Empty<AnalyticsComponentTrendPoint>()));
|
||||
|
||||
public Task<PlatformAvailableLocalesResponse> GetAvailableLocalesAsync(string tenant, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(new PlatformAvailableLocalesResponse(
|
||||
new[] { "en-US", "de-DE", "bg-BG", "ru-RU", "es-ES", "fr-FR", "uk-UA", "zh-TW", "zh-CN" },
|
||||
9));
|
||||
|
||||
public Task<PlatformLanguagePreferenceResponse> GetLanguagePreferenceAsync(string tenant, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(new PlatformLanguagePreferenceResponse(tenant, "stub-actor", null, DateTimeOffset.UtcNow, "stub"));
|
||||
|
||||
public Task<PlatformLanguagePreferenceResponse> SetLanguagePreferenceAsync(string tenant, string locale, CancellationToken cancellationToken)
|
||||
{
|
||||
LastLanguagePreferenceSet = (tenant, locale);
|
||||
return Task.FromResult(new PlatformLanguagePreferenceResponse(tenant, "stub-actor", locale, DateTimeOffset.UtcNow, "stub"));
|
||||
}
|
||||
|
||||
public Task<WitnessListResponse> ListWitnessesAsync(WitnessListRequest request, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(new WitnessListResponse());
|
||||
|
||||
@@ -4958,6 +4973,49 @@ spec:
|
||||
|
||||
public Task<Stream> DownloadWitnessAsync(string witnessId, WitnessExportFormat format, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<Stream>(new MemoryStream(Encoding.UTF8.GetBytes("{}")));
|
||||
|
||||
// CLI-IDP-001: Identity provider management stubs
|
||||
public Task<IReadOnlyList<IdentityProviderDto>> ListIdentityProvidersAsync(CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IReadOnlyList<IdentityProviderDto>>(Array.Empty<IdentityProviderDto>());
|
||||
|
||||
public Task<IdentityProviderDto?> GetIdentityProviderAsync(string name, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IdentityProviderDto?>(null);
|
||||
|
||||
public Task<IdentityProviderDto> CreateIdentityProviderAsync(CreateIdentityProviderRequest request, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(new IdentityProviderDto
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = request.Name,
|
||||
Type = request.Type,
|
||||
Enabled = request.Enabled,
|
||||
Configuration = request.Configuration,
|
||||
Description = request.Description,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
public Task<IdentityProviderDto> UpdateIdentityProviderAsync(Guid id, UpdateIdentityProviderRequest request, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(new IdentityProviderDto
|
||||
{
|
||||
Id = id,
|
||||
Name = "updated",
|
||||
Type = "standard",
|
||||
Enabled = request.Enabled ?? true,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
public Task<bool> DeleteIdentityProviderAsync(Guid id, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(true);
|
||||
|
||||
public Task<TestConnectionResult> TestIdentityProviderConnectionAsync(TestConnectionRequest request, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(new TestConnectionResult { Success = true, Message = "Connection successful", LatencyMs = 42 });
|
||||
|
||||
public Task<bool> EnableIdentityProviderAsync(Guid id, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(true);
|
||||
|
||||
public Task<bool> DisableIdentityProviderAsync(Guid id, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(true);
|
||||
}
|
||||
|
||||
private sealed class StubExecutor : IScannerExecutor
|
||||
@@ -5177,5 +5235,83 @@ spec:
|
||||
AnsiConsole.Console = originalConsole;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleTenantsLocaleListAsync_AsJsonIncludesUkrainianLocale()
|
||||
{
|
||||
var originalExit = Environment.ExitCode;
|
||||
var options = new StellaOpsCliOptions
|
||||
{
|
||||
BackendUrl = "https://platform.local",
|
||||
ResultsDirectory = Path.Combine(Path.GetTempPath(), $"stellaops-cli-results-{Guid.NewGuid():N}")
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
|
||||
var provider = BuildServiceProvider(backend, options: options);
|
||||
|
||||
var output = await CaptureTestConsoleAsync(async _ =>
|
||||
{
|
||||
await CommandHandlers.HandleTenantsLocaleListAsync(
|
||||
provider,
|
||||
options,
|
||||
tenant: "tenant-alpha",
|
||||
json: true,
|
||||
verbose: false,
|
||||
cancellationToken: CancellationToken.None);
|
||||
});
|
||||
|
||||
Assert.Equal(0, Environment.ExitCode);
|
||||
using var document = JsonDocument.Parse(output.PlainBuffer);
|
||||
var locales = document.RootElement.GetProperty("locales")
|
||||
.EnumerateArray()
|
||||
.Select(static locale => locale.GetString())
|
||||
.Where(static locale => !string.IsNullOrWhiteSpace(locale))
|
||||
.Select(static locale => locale!)
|
||||
.ToArray();
|
||||
Assert.Contains("uk-UA", locales, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.ExitCode = originalExit;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleTenantsLocaleSetAsync_RejectsLocaleOutsideCatalog()
|
||||
{
|
||||
var originalExit = Environment.ExitCode;
|
||||
var options = new StellaOpsCliOptions
|
||||
{
|
||||
BackendUrl = "https://platform.local",
|
||||
ResultsDirectory = Path.Combine(Path.GetTempPath(), $"stellaops-cli-results-{Guid.NewGuid():N}")
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
|
||||
var provider = BuildServiceProvider(backend, options: options);
|
||||
|
||||
var output = await CaptureTestConsoleAsync(async _ =>
|
||||
{
|
||||
await CommandHandlers.HandleTenantsLocaleSetAsync(
|
||||
provider,
|
||||
options,
|
||||
locale: "xx-XX",
|
||||
tenant: "tenant-alpha",
|
||||
json: false,
|
||||
verbose: false,
|
||||
cancellationToken: CancellationToken.None);
|
||||
});
|
||||
|
||||
Assert.Equal(1, Environment.ExitCode);
|
||||
Assert.Null(backend.LastLanguagePreferenceSet);
|
||||
Assert.Contains("not available", output.Combined, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.ExitCode = originalExit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moq;
|
||||
using StellaOps.Cli.Commands;
|
||||
using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cli.Tests.Commands;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class IdentityProviderCommandGroupTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ListCommand_JsonOutput_CallsListIdentityProvidersAsync()
|
||||
{
|
||||
var providers = new List<IdentityProviderDto>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("00000000-0000-0000-0000-000000000001"),
|
||||
Name = "corp-ldap",
|
||||
Type = "ldap",
|
||||
Enabled = true,
|
||||
Configuration = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Host"] = "ldap.corp.example.com",
|
||||
["Port"] = "636"
|
||||
},
|
||||
HealthStatus = "healthy",
|
||||
CreatedAt = DateTimeOffset.Parse("2026-02-20T10:00:00Z", CultureInfo.InvariantCulture),
|
||||
UpdatedAt = DateTimeOffset.Parse("2026-02-20T10:00:00Z", CultureInfo.InvariantCulture)
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("00000000-0000-0000-0000-000000000002"),
|
||||
Name = "okta-oidc",
|
||||
Type = "oidc",
|
||||
Enabled = false,
|
||||
Configuration = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Authority"] = "https://okta.example.com"
|
||||
},
|
||||
HealthStatus = "unknown",
|
||||
CreatedAt = DateTimeOffset.Parse("2026-02-21T12:00:00Z", CultureInfo.InvariantCulture),
|
||||
UpdatedAt = DateTimeOffset.Parse("2026-02-21T12:00:00Z", CultureInfo.InvariantCulture)
|
||||
}
|
||||
};
|
||||
|
||||
var backend = new Mock<IBackendOperationsClient>(MockBehavior.Strict);
|
||||
backend
|
||||
.Setup(c => c.ListIdentityProvidersAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(providers);
|
||||
|
||||
using var services = new ServiceCollection()
|
||||
.AddSingleton(backend.Object)
|
||||
.BuildServiceProvider();
|
||||
|
||||
var root = new RootCommand();
|
||||
root.Add(IdentityProviderCommandGroup.BuildIdentityProviderCommand(services, CancellationToken.None));
|
||||
|
||||
var invocation = await InvokeWithCapturedConsoleAsync(root, "identity-providers list --json");
|
||||
|
||||
Assert.Equal(0, invocation.ExitCode);
|
||||
|
||||
using var doc = JsonDocument.Parse(invocation.StdOut);
|
||||
var arr = doc.RootElement;
|
||||
Assert.Equal(2, arr.GetArrayLength());
|
||||
Assert.Equal("corp-ldap", arr[0].GetProperty("name").GetString());
|
||||
Assert.Equal("ldap", arr[0].GetProperty("type").GetString());
|
||||
Assert.True(arr[0].GetProperty("enabled").GetBoolean());
|
||||
Assert.Equal("okta-oidc", arr[1].GetProperty("name").GetString());
|
||||
Assert.Equal("oidc", arr[1].GetProperty("type").GetString());
|
||||
Assert.False(arr[1].GetProperty("enabled").GetBoolean());
|
||||
|
||||
backend.VerifyAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddCommand_LdapType_CallsCreateWithCorrectConfig()
|
||||
{
|
||||
CreateIdentityProviderRequest? capturedRequest = null;
|
||||
|
||||
var backend = new Mock<IBackendOperationsClient>(MockBehavior.Strict);
|
||||
backend
|
||||
.Setup(c => c.CreateIdentityProviderAsync(
|
||||
It.IsAny<CreateIdentityProviderRequest>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<CreateIdentityProviderRequest, CancellationToken>((req, _) => capturedRequest = req)
|
||||
.ReturnsAsync(new IdentityProviderDto
|
||||
{
|
||||
Id = Guid.Parse("00000000-0000-0000-0000-000000000003"),
|
||||
Name = "test-ldap",
|
||||
Type = "ldap",
|
||||
Enabled = true,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
using var services = new ServiceCollection()
|
||||
.AddSingleton(backend.Object)
|
||||
.BuildServiceProvider();
|
||||
|
||||
var root = new RootCommand();
|
||||
root.Add(IdentityProviderCommandGroup.BuildIdentityProviderCommand(services, CancellationToken.None));
|
||||
|
||||
var invocation = await InvokeWithCapturedConsoleAsync(
|
||||
root,
|
||||
"identity-providers add --name test-ldap --type ldap --host ldap.example.com --port 636 --bind-dn cn=admin,dc=example,dc=com --search-base ou=users,dc=example,dc=com --use-ssl true");
|
||||
|
||||
Assert.Equal(0, invocation.ExitCode);
|
||||
Assert.NotNull(capturedRequest);
|
||||
Assert.Equal("test-ldap", capturedRequest!.Name);
|
||||
Assert.Equal("ldap", capturedRequest.Type);
|
||||
Assert.True(capturedRequest.Enabled);
|
||||
Assert.Equal("ldap.example.com", capturedRequest.Configuration["Host"]);
|
||||
Assert.Equal("636", capturedRequest.Configuration["Port"]);
|
||||
Assert.Equal("cn=admin,dc=example,dc=com", capturedRequest.Configuration["BindDn"]);
|
||||
Assert.Equal("ou=users,dc=example,dc=com", capturedRequest.Configuration["SearchBase"]);
|
||||
Assert.Equal("true", capturedRequest.Configuration["UseSsl"]);
|
||||
|
||||
Assert.Contains("created successfully", invocation.StdOut, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
backend.VerifyAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddCommand_OidcType_CallsCreateWithCorrectConfig()
|
||||
{
|
||||
CreateIdentityProviderRequest? capturedRequest = null;
|
||||
|
||||
var backend = new Mock<IBackendOperationsClient>(MockBehavior.Strict);
|
||||
backend
|
||||
.Setup(c => c.CreateIdentityProviderAsync(
|
||||
It.IsAny<CreateIdentityProviderRequest>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<CreateIdentityProviderRequest, CancellationToken>((req, _) => capturedRequest = req)
|
||||
.ReturnsAsync(new IdentityProviderDto
|
||||
{
|
||||
Id = Guid.Parse("00000000-0000-0000-0000-000000000004"),
|
||||
Name = "okta-prod",
|
||||
Type = "oidc",
|
||||
Enabled = true,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
using var services = new ServiceCollection()
|
||||
.AddSingleton(backend.Object)
|
||||
.BuildServiceProvider();
|
||||
|
||||
var root = new RootCommand();
|
||||
root.Add(IdentityProviderCommandGroup.BuildIdentityProviderCommand(services, CancellationToken.None));
|
||||
|
||||
var invocation = await InvokeWithCapturedConsoleAsync(
|
||||
root,
|
||||
"identity-providers add --name okta-prod --type oidc --authority https://okta.example.com --client-id my-client --client-secret my-secret");
|
||||
|
||||
Assert.Equal(0, invocation.ExitCode);
|
||||
Assert.NotNull(capturedRequest);
|
||||
Assert.Equal("okta-prod", capturedRequest!.Name);
|
||||
Assert.Equal("oidc", capturedRequest.Type);
|
||||
Assert.Equal("https://okta.example.com", capturedRequest.Configuration["Authority"]);
|
||||
Assert.Equal("my-client", capturedRequest.Configuration["ClientId"]);
|
||||
Assert.Equal("my-secret", capturedRequest.Configuration["ClientSecret"]);
|
||||
|
||||
backend.VerifyAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveCommand_CallsDeleteIdentityProviderAsync()
|
||||
{
|
||||
var providerId = Guid.Parse("00000000-0000-0000-0000-000000000005");
|
||||
|
||||
var backend = new Mock<IBackendOperationsClient>(MockBehavior.Strict);
|
||||
backend
|
||||
.Setup(c => c.GetIdentityProviderAsync("corp-ldap", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new IdentityProviderDto
|
||||
{
|
||||
Id = providerId,
|
||||
Name = "corp-ldap",
|
||||
Type = "ldap",
|
||||
Enabled = true,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
backend
|
||||
.Setup(c => c.DeleteIdentityProviderAsync(providerId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
using var services = new ServiceCollection()
|
||||
.AddSingleton(backend.Object)
|
||||
.BuildServiceProvider();
|
||||
|
||||
var root = new RootCommand();
|
||||
root.Add(IdentityProviderCommandGroup.BuildIdentityProviderCommand(services, CancellationToken.None));
|
||||
|
||||
var invocation = await InvokeWithCapturedConsoleAsync(root, "identity-providers remove corp-ldap");
|
||||
|
||||
Assert.Equal(0, invocation.ExitCode);
|
||||
Assert.Contains("removed", invocation.StdOut, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
backend.VerifyAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveCommand_NotFound_SetsExitCodeOne()
|
||||
{
|
||||
var backend = new Mock<IBackendOperationsClient>(MockBehavior.Strict);
|
||||
backend
|
||||
.Setup(c => c.GetIdentityProviderAsync("missing", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((IdentityProviderDto?)null);
|
||||
|
||||
using var services = new ServiceCollection()
|
||||
.AddSingleton(backend.Object)
|
||||
.BuildServiceProvider();
|
||||
|
||||
var root = new RootCommand();
|
||||
root.Add(IdentityProviderCommandGroup.BuildIdentityProviderCommand(services, CancellationToken.None));
|
||||
|
||||
var invocation = await InvokeWithCapturedConsoleAsync(root, "identity-providers remove missing");
|
||||
|
||||
Assert.Equal(1, invocation.ExitCode);
|
||||
Assert.Contains("not found", invocation.StdErr, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
backend.VerifyAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnableCommand_CallsEnableIdentityProviderAsync()
|
||||
{
|
||||
var providerId = Guid.Parse("00000000-0000-0000-0000-000000000006");
|
||||
|
||||
var backend = new Mock<IBackendOperationsClient>(MockBehavior.Strict);
|
||||
backend
|
||||
.Setup(c => c.GetIdentityProviderAsync("my-saml", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new IdentityProviderDto
|
||||
{
|
||||
Id = providerId,
|
||||
Name = "my-saml",
|
||||
Type = "saml",
|
||||
Enabled = false,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
backend
|
||||
.Setup(c => c.EnableIdentityProviderAsync(providerId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
using var services = new ServiceCollection()
|
||||
.AddSingleton(backend.Object)
|
||||
.BuildServiceProvider();
|
||||
|
||||
var root = new RootCommand();
|
||||
root.Add(IdentityProviderCommandGroup.BuildIdentityProviderCommand(services, CancellationToken.None));
|
||||
|
||||
var invocation = await InvokeWithCapturedConsoleAsync(root, "identity-providers enable my-saml");
|
||||
|
||||
Assert.Equal(0, invocation.ExitCode);
|
||||
Assert.Contains("enabled", invocation.StdOut, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
backend.VerifyAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableCommand_CallsDisableIdentityProviderAsync()
|
||||
{
|
||||
var providerId = Guid.Parse("00000000-0000-0000-0000-000000000007");
|
||||
|
||||
var backend = new Mock<IBackendOperationsClient>(MockBehavior.Strict);
|
||||
backend
|
||||
.Setup(c => c.GetIdentityProviderAsync("my-oidc", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new IdentityProviderDto
|
||||
{
|
||||
Id = providerId,
|
||||
Name = "my-oidc",
|
||||
Type = "oidc",
|
||||
Enabled = true,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
backend
|
||||
.Setup(c => c.DisableIdentityProviderAsync(providerId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
using var services = new ServiceCollection()
|
||||
.AddSingleton(backend.Object)
|
||||
.BuildServiceProvider();
|
||||
|
||||
var root = new RootCommand();
|
||||
root.Add(IdentityProviderCommandGroup.BuildIdentityProviderCommand(services, CancellationToken.None));
|
||||
|
||||
var invocation = await InvokeWithCapturedConsoleAsync(root, "identity-providers disable my-oidc");
|
||||
|
||||
Assert.Equal(0, invocation.ExitCode);
|
||||
Assert.Contains("disabled", invocation.StdOut, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
backend.VerifyAll();
|
||||
}
|
||||
|
||||
private static async Task<CommandInvocationResult> InvokeWithCapturedConsoleAsync(
|
||||
RootCommand root,
|
||||
string commandLine)
|
||||
{
|
||||
var originalOut = Console.Out;
|
||||
var originalError = Console.Error;
|
||||
var originalExitCode = Environment.ExitCode;
|
||||
Environment.ExitCode = 0;
|
||||
|
||||
var stdout = new StringWriter(CultureInfo.InvariantCulture);
|
||||
var stderr = new StringWriter(CultureInfo.InvariantCulture);
|
||||
try
|
||||
{
|
||||
Console.SetOut(stdout);
|
||||
Console.SetError(stderr);
|
||||
var exitCode = await root.Parse(commandLine).InvokeAsync();
|
||||
var capturedExitCode = Environment.ExitCode != 0 ? Environment.ExitCode : exitCode;
|
||||
return new CommandInvocationResult(capturedExitCode, stdout.ToString(), stderr.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
Console.SetError(originalError);
|
||||
Environment.ExitCode = originalExitCode;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record CommandInvocationResult(int ExitCode, string StdOut, string StdErr);
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cli.Tests.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// CLI integration tests for identity provider commands against real IDP containers.
|
||||
/// Requires: docker compose -f devops/compose/docker-compose.idp-testing.yml --profile idp up -d
|
||||
/// Execute: dotnet test --filter "FullyQualifiedName~IdentityProviderIntegrationTests"
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Integration)]
|
||||
[Collection("IdpContainerTests")]
|
||||
public sealed class IdentityProviderIntegrationTests
|
||||
{
|
||||
private const string LdapHost = "localhost";
|
||||
private const int LdapPort = 3389;
|
||||
private const string KeycloakBaseUrl = "http://localhost:8280";
|
||||
|
||||
/// <summary>
|
||||
/// Validates the CLI model DTOs can be constructed and their properties match API contract.
|
||||
/// This is a local-only test that does not require containers.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IdentityProviderDto_PropertiesAreAccessible()
|
||||
{
|
||||
var dto = new IdentityProviderDto
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "test-provider",
|
||||
Type = "ldap",
|
||||
Enabled = true,
|
||||
Configuration = new Dictionary<string, string?>
|
||||
{
|
||||
["host"] = "ldap.test",
|
||||
["port"] = "389"
|
||||
},
|
||||
Description = "Test",
|
||||
HealthStatus = "healthy",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
Assert.Equal("test-provider", dto.Name);
|
||||
Assert.Equal("ldap", dto.Type);
|
||||
Assert.True(dto.Enabled);
|
||||
Assert.Equal("ldap.test", dto.Configuration["host"]);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateIdentityProviderRequest_CanBeConstructed()
|
||||
{
|
||||
var request = new CreateIdentityProviderRequest
|
||||
{
|
||||
Name = "my-ldap",
|
||||
Type = "ldap",
|
||||
Enabled = true,
|
||||
Configuration = new Dictionary<string, string?>
|
||||
{
|
||||
["host"] = "ldap.example.com",
|
||||
["port"] = "636",
|
||||
["bindDn"] = "cn=admin,dc=example,dc=com",
|
||||
["bindPassword"] = "secret",
|
||||
["searchBase"] = "dc=example,dc=com"
|
||||
},
|
||||
Description = "Production LDAP"
|
||||
};
|
||||
|
||||
Assert.Equal("my-ldap", request.Name);
|
||||
Assert.Equal("ldap", request.Type);
|
||||
Assert.Equal(5, request.Configuration.Count);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TestConnectionRequest_SamlType()
|
||||
{
|
||||
var request = new TestConnectionRequest
|
||||
{
|
||||
Type = "saml",
|
||||
Configuration = new Dictionary<string, string?>
|
||||
{
|
||||
["spEntityId"] = "stellaops-sp",
|
||||
["idpEntityId"] = "https://idp.example.com",
|
||||
["idpMetadataUrl"] = "https://idp.example.com/metadata"
|
||||
}
|
||||
};
|
||||
|
||||
Assert.Equal("saml", request.Type);
|
||||
Assert.Equal(3, request.Configuration.Count);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TestConnectionRequest_OidcType()
|
||||
{
|
||||
var request = new TestConnectionRequest
|
||||
{
|
||||
Type = "oidc",
|
||||
Configuration = new Dictionary<string, string?>
|
||||
{
|
||||
["authority"] = "https://auth.example.com",
|
||||
["clientId"] = "stellaops",
|
||||
["clientSecret"] = "secret"
|
||||
}
|
||||
};
|
||||
|
||||
Assert.Equal("oidc", request.Type);
|
||||
Assert.Equal(3, request.Configuration.Count);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TestConnectionResult_SuccessAndFailure()
|
||||
{
|
||||
var success = new TestConnectionResult
|
||||
{
|
||||
Success = true,
|
||||
Message = "Connection successful",
|
||||
LatencyMs = 42
|
||||
};
|
||||
Assert.True(success.Success);
|
||||
Assert.Equal(42, success.LatencyMs);
|
||||
|
||||
var failure = new TestConnectionResult
|
||||
{
|
||||
Success = false,
|
||||
Message = "Connection timed out",
|
||||
LatencyMs = 10000
|
||||
};
|
||||
Assert.False(failure.Success);
|
||||
}
|
||||
|
||||
// --- Container-dependent tests below ---
|
||||
|
||||
[Fact(Skip = "Requires docker compose idp containers")]
|
||||
public async Task AddLdapProvider_ListShowsIt()
|
||||
{
|
||||
// This test would exercise the CLI backend client against the Platform API
|
||||
// which connects to real OpenLDAP container
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact(Skip = "Requires docker compose idp containers")]
|
||||
public async Task AddSamlProvider_WithKeycloakMetadata()
|
||||
{
|
||||
// Would test creating a SAML provider pointing to Keycloak's metadata URL
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact(Skip = "Requires docker compose idp containers")]
|
||||
public async Task AddOidcProvider_WithKeycloakDiscovery()
|
||||
{
|
||||
// Would test creating an OIDC provider pointing to Keycloak's OIDC endpoint
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact(Skip = "Requires docker compose idp containers")]
|
||||
public async Task TestConnection_LiveLdap_Succeeds()
|
||||
{
|
||||
// Would test the test-connection command against real OpenLDAP
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact(Skip = "Requires docker compose idp containers")]
|
||||
public async Task DisableAndEnable_Provider()
|
||||
{
|
||||
// Would test the disable/enable commands
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact(Skip = "Requires docker compose idp containers")]
|
||||
public async Task RemoveProvider_RemovesFromList()
|
||||
{
|
||||
// Would test the remove command
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -50,3 +50,5 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
|
||||
|
||||
| PAPI-005-TESTS | DONE | SPRINT_20260210_005 - DevPortal portable-v1 verifier matrix hardened with manifest/DSSE/Rekor/Parquet fail-closed tests; CLI suite passed (1182 passed) on 2026-02-10. |
|
||||
| SPRINT_20260224_004-LOC-303-T | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: updated `CommandHandlersTests` backend stubs for new locale preference client methods; full-suite execution reached `1196/1201` with unrelated pre-existing failures in migration/knowledge-search/risk-budget test lanes. |
|
||||
| SPRINT_20260224_004-LOC-308-CLI-T | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: added command-handler coverage for locale catalog listing (`tenants locale list`) and unsupported locale rejection before preference writes. |
|
||||
|
||||
Reference in New Issue
Block a user