Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,110 @@
using System;
using System.IO;
using System.Text.Json;
using StellaOps.Cli.Configuration;
using Xunit;
namespace StellaOps.Cli.Tests.Configuration;
public sealed class CliBootstrapperTests : IDisposable
{
private readonly string _originalDirectory = Directory.GetCurrentDirectory();
private readonly string _tempDirectory = Path.Combine(Path.GetTempPath(), $"stellaops-cli-tests-{Guid.NewGuid():N}");
public CliBootstrapperTests()
{
Directory.CreateDirectory(_tempDirectory);
Directory.SetCurrentDirectory(_tempDirectory);
}
[Fact]
public void Build_UsesEnvironmentVariablesWhenPresent()
{
Environment.SetEnvironmentVariable("API_KEY", "env-key");
Environment.SetEnvironmentVariable("STELLAOPS_BACKEND_URL", "https://env-backend.example");
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_URL", "https://authority.env");
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_CLIENT_ID", "cli-env");
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_SCOPE", "concelier.jobs.trigger");
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_ENABLE_RETRIES", "false");
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_RETRY_DELAYS", "00:00:02,00:00:05");
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_ALLOW_OFFLINE_CACHE_FALLBACK", "false");
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_OFFLINE_CACHE_TOLERANCE", "00:20:00");
try
{
var (options, _) = CliBootstrapper.Build(Array.Empty<string>());
Assert.Equal("env-key", options.ApiKey);
Assert.Equal("https://env-backend.example", options.BackendUrl);
Assert.Equal("https://authority.env", options.Authority.Url);
Assert.Equal("cli-env", options.Authority.ClientId);
Assert.Equal("concelier.jobs.trigger", options.Authority.Scope);
Assert.NotNull(options.Authority.Resilience);
Assert.False(options.Authority.Resilience.EnableRetries);
Assert.Equal(new[] { TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5) }, options.Authority.Resilience.RetryDelays);
Assert.False(options.Authority.Resilience.AllowOfflineCacheFallback);
Assert.Equal(TimeSpan.FromMinutes(20), options.Authority.Resilience.OfflineCacheTolerance);
}
finally
{
Environment.SetEnvironmentVariable("API_KEY", null);
Environment.SetEnvironmentVariable("STELLAOPS_BACKEND_URL", null);
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_URL", null);
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_CLIENT_ID", null);
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_SCOPE", null);
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_ENABLE_RETRIES", null);
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_RETRY_DELAYS", null);
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_ALLOW_OFFLINE_CACHE_FALLBACK", null);
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_OFFLINE_CACHE_TOLERANCE", null);
}
}
[Fact]
public void Build_FallsBackToAppSettings()
{
WriteAppSettings(new
{
StellaOps = new
{
ApiKey = "file-key",
BackendUrl = "https://file-backend.example",
Authority = new
{
Url = "https://authority.file",
ClientId = "cli-file",
Scope = "concelier.jobs.trigger"
}
}
});
var (options, _) = CliBootstrapper.Build(Array.Empty<string>());
Assert.Equal("file-key", options.ApiKey);
Assert.Equal("https://file-backend.example", options.BackendUrl);
Assert.Equal("https://authority.file", options.Authority.Url);
Assert.Equal("cli-file", options.Authority.ClientId);
}
public void Dispose()
{
Directory.SetCurrentDirectory(_originalDirectory);
if (Directory.Exists(_tempDirectory))
{
try
{
Directory.Delete(_tempDirectory, recursive: true);
}
catch
{
// Ignored.
}
}
}
private static void WriteAppSettings<T>(T payload)
{
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText("appsettings.json", json);
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.CommandLine;
using System.IO;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Plugins;
using Xunit;
namespace StellaOps.Cli.Tests.Plugins;
public sealed class CliCommandModuleLoaderTests
{
[Fact]
public void RegisterModules_LoadsNonCoreCommandsFromPlugin()
{
var options = new StellaOpsCliOptions();
var repoRoot = Path.GetFullPath(
Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
options.Plugins.BaseDirectory = repoRoot;
options.Plugins.Directory = "plugins/cli";
options.Plugins.ManifestSearchPattern = "manifest.json";
var services = new ServiceCollection()
.AddSingleton(options)
.BuildServiceProvider();
var logger = NullLoggerFactory.Instance.CreateLogger<CliCommandModuleLoader>();
var loader = new CliCommandModuleLoader(services, options, logger);
var root = new RootCommand();
var verbose = new Option<bool>("--verbose");
loader.RegisterModules(root, verbose, CancellationToken.None);
Assert.Contains(root.Children, command => string.Equals(command.Name, "excititor", StringComparison.Ordinal));
Assert.Contains(root.Children, command => string.Equals(command.Name, "runtime", StringComparison.Ordinal));
Assert.Contains(root.Children, command => string.Equals(command.Name, "offline", StringComparison.Ordinal));
}
}

View File

@@ -0,0 +1,29 @@
using StellaOps.Cli.Plugins;
using Xunit;
namespace StellaOps.Cli.Tests.Plugins;
public sealed class RestartOnlyCliPluginGuardTests
{
[Fact]
public void EnsureRegistrationAllowed_AllowsDuringStartup()
{
var guard = new RestartOnlyCliPluginGuard();
guard.EnsureRegistrationAllowed("./plugins/sample.dll");
guard.Seal();
// Re-registering known plug-ins after sealing should succeed.
guard.EnsureRegistrationAllowed("./plugins/sample.dll");
Assert.True(guard.IsSealed);
Assert.Single(guard.KnownPlugins);
}
[Fact]
public void EnsureRegistrationAllowed_ThrowsForUnknownAfterSeal()
{
var guard = new RestartOnlyCliPluginGuard();
guard.Seal();
Assert.Throws<InvalidOperationException>(() => guard.EnsureRegistrationAllowed("./plugins/new.dll"));
}
}

View File

@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services;
using Xunit;
namespace StellaOps.Cli.Tests.Services;
public sealed class AuthorityDiagnosticsReporterTests : IDisposable
{
private readonly string _originalDirectory = Directory.GetCurrentDirectory();
private readonly string _tempDirectory = Path.Combine(Path.GetTempPath(), $"stellaops-cli-tests-{Guid.NewGuid():N}");
public AuthorityDiagnosticsReporterTests()
{
Directory.CreateDirectory(_tempDirectory);
Directory.SetCurrentDirectory(_tempDirectory);
}
[Fact]
public void Emit_LogsWarning_WhenPasswordPolicyWeakened()
{
WriteAuthorityConfiguration(minimumLength: 8);
var (_, configuration) = CliBootstrapper.Build(Array.Empty<string>());
var logger = new ListLogger();
AuthorityDiagnosticsReporter.Emit(configuration, logger);
var warning = Assert.Single(logger.Entries, entry => entry.Level == LogLevel.Warning);
Assert.Contains("minimum length 8 < 12", warning.Message, StringComparison.OrdinalIgnoreCase);
Assert.Contains("standard.yaml", warning.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Emit_EmitsNoWarnings_WhenPasswordPolicyMeetsBaseline()
{
WriteAuthorityConfiguration(minimumLength: 12);
var (_, configuration) = CliBootstrapper.Build(Array.Empty<string>());
var logger = new ListLogger();
AuthorityDiagnosticsReporter.Emit(configuration, logger);
Assert.DoesNotContain(logger.Entries, entry => entry.Level >= LogLevel.Warning);
}
public void Dispose()
{
Directory.SetCurrentDirectory(_originalDirectory);
try
{
if (Directory.Exists(_tempDirectory))
{
Directory.Delete(_tempDirectory, recursive: true);
}
}
catch
{
// Ignored.
}
}
private static void WriteAuthorityConfiguration(int minimumLength)
{
var payload = new
{
Authority = new
{
Plugins = new
{
ConfigurationDirectory = "plugins",
Descriptors = new
{
standard = new
{
AssemblyName = "StellaOps.Authority.Plugin.Standard",
Enabled = true,
ConfigFile = "standard.yaml"
}
}
}
}
};
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText("appsettings.json", json);
var pluginDirectory = Path.Combine(Directory.GetCurrentDirectory(), "plugins");
Directory.CreateDirectory(pluginDirectory);
var pluginConfig = $"""
bootstrapUser:
username: "admin"
password: "changeme"
passwordPolicy:
minimumLength: {minimumLength}
requireUppercase: true
requireLowercase: true
requireDigit: true
requireSymbol: true
""";
File.WriteAllText(Path.Combine(pluginDirectory, "standard.yaml"), pluginConfig);
}
private sealed class ListLogger : ILogger
{
public readonly record struct LogEntry(LogLevel Level, string Message);
public List<LogEntry> Entries { get; } = new();
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
var message = formatter(state, exception);
Entries.Add(new LogEntry(logLevel, message));
}
}
private sealed class NullScope : IDisposable
{
public static NullScope Instance { get; } = new();
public void Dispose()
{
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Spectre.Console.Testing" Version="0.48.0" />
<ProjectReference Include="../../StellaOps.Cli/StellaOps.Cli.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Text;
namespace StellaOps.Cli.Tests.Testing;
internal sealed class TempDirectory : IDisposable
{
public TempDirectory()
{
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"stellaops-cli-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(Path);
}
public string Path { get; }
public void Dispose()
{
try
{
if (Directory.Exists(Path))
{
Directory.Delete(Path, recursive: true);
}
}
catch
{
// ignored
}
}
}
internal sealed class TempFile : IDisposable
{
public TempFile(string fileName, byte[] contents)
{
var directory = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"stellaops-cli-file-{Guid.NewGuid():N}");
Directory.CreateDirectory(directory);
Path = System.IO.Path.Combine(directory, fileName);
File.WriteAllBytes(Path, contents);
}
public string Path { get; }
public void Dispose()
{
try
{
if (File.Exists(Path))
{
File.Delete(Path);
}
var directory = System.IO.Path.GetDirectoryName(Path);
if (!string.IsNullOrEmpty(directory) && Directory.Exists(directory))
{
Directory.Delete(directory, recursive: true);
}
}
catch
{
// ignored intentionally
}
}
}
internal sealed class StubHttpMessageHandler : HttpMessageHandler
{
private readonly Queue<Func<HttpRequestMessage, CancellationToken, HttpResponseMessage>> _responses;
public StubHttpMessageHandler(params Func<HttpRequestMessage, CancellationToken, HttpResponseMessage>[] handlers)
{
if (handlers is null || handlers.Length == 0)
{
throw new ArgumentException("At least one handler must be provided.", nameof(handlers));
}
_responses = new Queue<Func<HttpRequestMessage, CancellationToken, HttpResponseMessage>>(handlers);
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var factory = _responses.Count > 1 ? _responses.Dequeue() : _responses.Peek();
return Task.FromResult(factory(request, cancellationToken));
}
}

View File

@@ -0,0 +1,10 @@
namespace StellaOps.Cli.Tests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}

View File

@@ -0,0 +1,3 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json"
}