Initial commit (history squashed)

This commit is contained in:
master
2025-10-07 10:14:21 +03:00
commit 016c5a3fe7
1132 changed files with 117842 additions and 0 deletions

View File

@@ -0,0 +1,533 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.Client;
using StellaOps.Cli.Commands;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Telemetry;
using StellaOps.Cli.Tests.Testing;
namespace StellaOps.Cli.Tests.Commands;
public sealed class CommandHandlersTests
{
[Fact]
public async Task HandleExportJobAsync_SetsExitCodeZeroOnSuccess()
{
var original = Environment.ExitCode;
try
{
var backend = new StubBackendClient(new JobTriggerResult(true, "Accepted", "/jobs/export:json/1", null));
var provider = BuildServiceProvider(backend);
await CommandHandlers.HandleExportJobAsync(
provider,
format: "json",
delta: false,
publishFull: null,
publishDelta: null,
includeFull: null,
includeDelta: null,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.Equal("export:json", backend.LastJobKind);
}
finally
{
Environment.ExitCode = original;
}
}
[Fact]
public async Task HandleMergeJobAsync_SetsExitCodeOnFailure()
{
var original = Environment.ExitCode;
try
{
var backend = new StubBackendClient(new JobTriggerResult(false, "Job already running", null, null));
var provider = BuildServiceProvider(backend);
await CommandHandlers.HandleMergeJobAsync(provider, verbose: false, CancellationToken.None);
Assert.Equal(1, Environment.ExitCode);
Assert.Equal("merge:reconcile", backend.LastJobKind);
}
finally
{
Environment.ExitCode = original;
}
}
[Fact]
public async Task HandleScannerRunAsync_AutomaticallyUploadsResults()
{
using var tempDir = new TempDirectory();
var resultsFile = Path.Combine(tempDir.Path, "results", "scan.json");
var backend = new StubBackendClient(new JobTriggerResult(true, "Accepted", null, null));
var metadataFile = Path.Combine(tempDir.Path, "results", "scan-run.json");
var executor = new StubExecutor(new ScannerExecutionResult(0, resultsFile, metadataFile));
var options = new StellaOpsCliOptions
{
ResultsDirectory = Path.Combine(tempDir.Path, "results")
};
var provider = BuildServiceProvider(backend, executor, new StubInstaller(), options);
Directory.CreateDirectory(Path.Combine(tempDir.Path, "target"));
var original = Environment.ExitCode;
try
{
await CommandHandlers.HandleScannerRunAsync(
provider,
runner: "docker",
entry: "scanner-image",
targetDirectory: Path.Combine(tempDir.Path, "target"),
arguments: Array.Empty<string>(),
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.Equal(resultsFile, backend.LastUploadPath);
Assert.True(File.Exists(metadataFile));
}
finally
{
Environment.ExitCode = original;
}
}
[Fact]
public async Task HandleAuthLoginAsync_UsesClientCredentialsFlow()
{
var original = Environment.ExitCode;
using var tempDir = new TempDirectory();
try
{
var options = new StellaOpsCliOptions
{
ResultsDirectory = Path.Combine(tempDir.Path, "results"),
Authority = new StellaOpsCliAuthorityOptions
{
Url = "https://authority.example",
ClientId = "cli",
ClientSecret = "secret",
Scope = "feedser.jobs.trigger",
TokenCacheDirectory = tempDir.Path
}
};
var tokenClient = new StubTokenClient();
var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: tokenClient);
await CommandHandlers.HandleAuthLoginAsync(provider, options, verbose: false, force: false, cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.Equal(1, tokenClient.ClientCredentialRequests);
Assert.NotNull(tokenClient.CachedEntry);
}
finally
{
Environment.ExitCode = original;
}
}
[Fact]
public async Task HandleAuthLoginAsync_FailsWhenPasswordMissing()
{
var original = Environment.ExitCode;
using var tempDir = new TempDirectory();
try
{
var options = new StellaOpsCliOptions
{
ResultsDirectory = Path.Combine(tempDir.Path, "results"),
Authority = new StellaOpsCliAuthorityOptions
{
Url = "https://authority.example",
ClientId = "cli",
Username = "user",
TokenCacheDirectory = tempDir.Path
}
};
var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: new StubTokenClient());
await CommandHandlers.HandleAuthLoginAsync(provider, options, verbose: false, force: false, cancellationToken: CancellationToken.None);
Assert.Equal(1, Environment.ExitCode);
}
finally
{
Environment.ExitCode = original;
}
}
[Fact]
public async Task HandleAuthStatusAsync_ReportsMissingToken()
{
var original = Environment.ExitCode;
using var tempDir = new TempDirectory();
try
{
var options = new StellaOpsCliOptions
{
ResultsDirectory = Path.Combine(tempDir.Path, "results"),
Authority = new StellaOpsCliAuthorityOptions
{
Url = "https://authority.example",
ClientId = "cli",
TokenCacheDirectory = tempDir.Path
}
};
var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: new StubTokenClient());
await CommandHandlers.HandleAuthStatusAsync(provider, options, verbose: false, cancellationToken: CancellationToken.None);
Assert.Equal(1, Environment.ExitCode);
}
finally
{
Environment.ExitCode = original;
}
}
[Fact]
public async Task HandleAuthStatusAsync_ReportsCachedToken()
{
var original = Environment.ExitCode;
using var tempDir = new TempDirectory();
try
{
var options = new StellaOpsCliOptions
{
ResultsDirectory = Path.Combine(tempDir.Path, "results"),
Authority = new StellaOpsCliAuthorityOptions
{
Url = "https://authority.example",
ClientId = "cli",
TokenCacheDirectory = tempDir.Path
}
};
var tokenClient = new StubTokenClient();
tokenClient.CachedEntry = new StellaOpsTokenCacheEntry(
"token",
"Bearer",
DateTimeOffset.UtcNow.AddMinutes(30),
new[] { StellaOpsScopes.FeedserJobsTrigger });
var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: tokenClient);
await CommandHandlers.HandleAuthStatusAsync(provider, options, verbose: true, cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
}
finally
{
Environment.ExitCode = original;
}
}
[Fact]
public async Task HandleAuthWhoAmIAsync_ReturnsErrorWhenTokenMissing()
{
var original = Environment.ExitCode;
using var tempDir = new TempDirectory();
try
{
var options = new StellaOpsCliOptions
{
ResultsDirectory = Path.Combine(tempDir.Path, "results"),
Authority = new StellaOpsCliAuthorityOptions
{
Url = "https://authority.example",
ClientId = "cli",
TokenCacheDirectory = tempDir.Path
}
};
var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: new StubTokenClient());
await CommandHandlers.HandleAuthWhoAmIAsync(provider, options, verbose: false, cancellationToken: CancellationToken.None);
Assert.Equal(1, Environment.ExitCode);
}
finally
{
Environment.ExitCode = original;
}
}
[Fact]
public async Task HandleAuthWhoAmIAsync_ReportsClaimsForJwtToken()
{
var original = Environment.ExitCode;
using var tempDir = new TempDirectory();
try
{
var options = new StellaOpsCliOptions
{
ResultsDirectory = Path.Combine(tempDir.Path, "results"),
Authority = new StellaOpsCliAuthorityOptions
{
Url = "https://authority.example",
ClientId = "cli",
TokenCacheDirectory = tempDir.Path
}
};
var tokenClient = new StubTokenClient();
tokenClient.CachedEntry = new StellaOpsTokenCacheEntry(
CreateUnsignedJwt(
("sub", "cli-user"),
("aud", "feedser"),
("iss", "https://authority.example"),
("iat", 1_700_000_000),
("nbf", 1_700_000_000)),
"Bearer",
DateTimeOffset.UtcNow.AddMinutes(30),
new[] { StellaOpsScopes.FeedserJobsTrigger });
var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: tokenClient);
await CommandHandlers.HandleAuthWhoAmIAsync(provider, options, verbose: true, cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
}
finally
{
Environment.ExitCode = original;
}
}
[Fact]
public async Task HandleAuthLogoutAsync_ClearsToken()
{
var original = Environment.ExitCode;
using var tempDir = new TempDirectory();
try
{
var options = new StellaOpsCliOptions
{
ResultsDirectory = Path.Combine(tempDir.Path, "results"),
Authority = new StellaOpsCliAuthorityOptions
{
Url = "https://authority.example",
ClientId = "cli",
TokenCacheDirectory = tempDir.Path
}
};
var tokenClient = new StubTokenClient();
tokenClient.CachedEntry = new StellaOpsTokenCacheEntry(
"token",
"Bearer",
DateTimeOffset.UtcNow.AddMinutes(5),
new[] { StellaOpsScopes.FeedserJobsTrigger });
var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: tokenClient);
await CommandHandlers.HandleAuthLogoutAsync(provider, options, verbose: true, cancellationToken: CancellationToken.None);
Assert.Null(tokenClient.CachedEntry);
Assert.Equal(1, tokenClient.ClearRequests);
Assert.Equal(0, Environment.ExitCode);
}
finally
{
Environment.ExitCode = original;
}
}
private static IServiceProvider BuildServiceProvider(
IBackendOperationsClient backend,
IScannerExecutor? executor = null,
IScannerInstaller? installer = null,
StellaOpsCliOptions? options = null,
IStellaOpsTokenClient? tokenClient = null)
{
var services = new ServiceCollection();
services.AddSingleton(backend);
services.AddSingleton<ILoggerFactory>(_ => LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug)));
services.AddSingleton(new VerbosityState());
var resolvedOptions = options ?? new StellaOpsCliOptions
{
ResultsDirectory = Path.Combine(Path.GetTempPath(), $"stellaops-cli-results-{Guid.NewGuid():N}")
};
services.AddSingleton(resolvedOptions);
var resolvedExecutor = executor ?? CreateDefaultExecutor();
services.AddSingleton<IScannerExecutor>(resolvedExecutor);
services.AddSingleton<IScannerInstaller>(installer ?? new StubInstaller());
if (tokenClient is not null)
{
services.AddSingleton(tokenClient);
}
return services.BuildServiceProvider();
}
private static IScannerExecutor CreateDefaultExecutor()
{
var tempResultsFile = Path.GetTempFileName();
var tempMetadataFile = Path.Combine(
Path.GetDirectoryName(tempResultsFile)!,
$"{Path.GetFileNameWithoutExtension(tempResultsFile)}-run.json");
return new StubExecutor(new ScannerExecutionResult(0, tempResultsFile, tempMetadataFile));
}
private sealed class StubBackendClient : IBackendOperationsClient
{
private readonly JobTriggerResult _result;
public StubBackendClient(JobTriggerResult result)
{
_result = result;
}
public string? LastJobKind { get; private set; }
public string? LastUploadPath { get; private set; }
public Task<ScannerArtifactResult> DownloadScannerAsync(string channel, string outputPath, bool overwrite, bool verbose, CancellationToken cancellationToken)
=> throw new NotImplementedException();
public Task UploadScanResultsAsync(string filePath, CancellationToken cancellationToken)
{
LastUploadPath = filePath;
return Task.CompletedTask;
}
public Task<JobTriggerResult> TriggerJobAsync(string jobKind, IDictionary<string, object?> parameters, CancellationToken cancellationToken)
{
LastJobKind = jobKind;
return Task.FromResult(_result);
}
}
private sealed class StubExecutor : IScannerExecutor
{
private readonly ScannerExecutionResult _result;
public StubExecutor(ScannerExecutionResult result)
{
_result = result;
}
public Task<ScannerExecutionResult> RunAsync(string runner, string entry, string targetDirectory, string resultsDirectory, IReadOnlyList<string> arguments, bool verbose, CancellationToken cancellationToken)
{
Directory.CreateDirectory(Path.GetDirectoryName(_result.ResultsPath)!);
if (!File.Exists(_result.ResultsPath))
{
File.WriteAllText(_result.ResultsPath, "{}");
}
Directory.CreateDirectory(Path.GetDirectoryName(_result.RunMetadataPath)!);
if (!File.Exists(_result.RunMetadataPath))
{
File.WriteAllText(_result.RunMetadataPath, "{}");
}
return Task.FromResult(_result);
}
}
private sealed class StubInstaller : IScannerInstaller
{
public Task InstallAsync(string artifactPath, bool verbose, CancellationToken cancellationToken)
=> Task.CompletedTask;
}
private sealed class StubTokenClient : IStellaOpsTokenClient
{
private readonly StellaOpsTokenResult _token;
public StubTokenClient()
{
_token = new StellaOpsTokenResult(
"token-123",
"Bearer",
DateTimeOffset.UtcNow.AddMinutes(30),
new[] { StellaOpsScopes.FeedserJobsTrigger });
}
public int ClientCredentialRequests { get; private set; }
public int PasswordRequests { get; private set; }
public int ClearRequests { get; private set; }
public StellaOpsTokenCacheEntry? CachedEntry { get; set; }
public ValueTask CacheTokenAsync(string key, StellaOpsTokenCacheEntry entry, CancellationToken cancellationToken = default)
{
CachedEntry = entry;
return ValueTask.CompletedTask;
}
public ValueTask ClearCachedTokenAsync(string key, CancellationToken cancellationToken = default)
{
ClearRequests++;
CachedEntry = null;
return ValueTask.CompletedTask;
}
public Task<JsonWebKeySet> GetJsonWebKeySetAsync(CancellationToken cancellationToken = default)
=> Task.FromResult(new JsonWebKeySet("{\"keys\":[]}"));
public ValueTask<StellaOpsTokenCacheEntry?> GetCachedTokenAsync(string key, CancellationToken cancellationToken = default)
=> ValueTask.FromResult(CachedEntry);
public Task<StellaOpsTokenResult> RequestClientCredentialsTokenAsync(string? scope = null, CancellationToken cancellationToken = default)
{
ClientCredentialRequests++;
return Task.FromResult(_token);
}
public Task<StellaOpsTokenResult> RequestPasswordTokenAsync(string username, string password, string? scope = null, CancellationToken cancellationToken = default)
{
PasswordRequests++;
return Task.FromResult(_token);
}
}
private static string CreateUnsignedJwt(params (string Key, object Value)[] claims)
{
var headerJson = "{\"alg\":\"none\",\"typ\":\"JWT\"}";
var payload = new Dictionary<string, object?>(StringComparer.Ordinal);
foreach (var claim in claims)
{
payload[claim.Key] = claim.Value;
}
var payloadJson = JsonSerializer.Serialize(payload);
return $"{Base64UrlEncode(headerJson)}.{Base64UrlEncode(payloadJson)}.";
}
private static string Base64UrlEncode(string value)
{
var bytes = Encoding.UTF8.GetBytes(value);
return Convert.ToBase64String(bytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
}