mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-31 01:40:42 +00:00
Esiur CLI
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
using Esiur.CLI;
|
||||
using Esiur.CLI.Authentication;
|
||||
using Esiur.CLI.Client;
|
||||
using Esiur.CLI.Configuration;
|
||||
|
||||
namespace Esiur.CLI.Tests;
|
||||
|
||||
public sealed class CommandTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ProfileUseListAndRemoveShareConfigurationService()
|
||||
{
|
||||
var directory = Directory.CreateTempSubdirectory("esiur-cli-command-");
|
||||
try
|
||||
{
|
||||
var store = new ConfigurationStore(Path.Combine(directory.FullName, "config.json"));
|
||||
await store.SaveAsync(new CliConfiguration
|
||||
{
|
||||
Profiles = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["production"] = new ConnectionProfile
|
||||
{
|
||||
Name = "production", Endpoint = "ep://host",
|
||||
},
|
||||
},
|
||||
}, default);
|
||||
var credentials = new PromptCredentialService();
|
||||
var stdout = new StringWriter();
|
||||
var stderr = new StringWriter();
|
||||
var app = new CliApplication(
|
||||
store, credentials, new EsiurSessionFactory(credentials),
|
||||
new ResourceInspectionService(), TextReader.Null, stdout, stderr);
|
||||
|
||||
Assert.Equal(ExitCodes.Success,
|
||||
await app.RunAsync(new[] { "profile", "use", "production" }, default));
|
||||
Assert.Equal("production", (await store.LoadAsync(default)).DefaultProfile);
|
||||
|
||||
Assert.Equal(ExitCodes.Success,
|
||||
await app.RunAsync(new[] { "profile", "list", "--output", "json" }, default));
|
||||
Assert.Contains("production", stdout.ToString());
|
||||
|
||||
Assert.Equal(ExitCodes.Success,
|
||||
await app.RunAsync(new[] { "profile", "remove", "production" }, default));
|
||||
var final = await store.LoadAsync(default);
|
||||
Assert.Empty(final.Profiles);
|
||||
Assert.Null(final.DefaultProfile);
|
||||
Assert.Empty(stderr.ToString());
|
||||
}
|
||||
finally { directory.Delete(true); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnknownCommandReturnsStableInvalidArgumentsCode()
|
||||
{
|
||||
var directory = Directory.CreateTempSubdirectory("esiur-cli-command-");
|
||||
try
|
||||
{
|
||||
var credentials = new PromptCredentialService();
|
||||
var app = new CliApplication(
|
||||
new ConfigurationStore(Path.Combine(directory.FullName, "config.json")),
|
||||
credentials, new EsiurSessionFactory(credentials), new ResourceInspectionService(),
|
||||
TextReader.Null, new StringWriter(), new StringWriter());
|
||||
Assert.Equal(ExitCodes.InvalidArguments,
|
||||
await app.RunAsync(new[] { "does-not-exist" }, default));
|
||||
}
|
||||
finally { directory.Delete(true); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using System.Text.Json;
|
||||
using Esiur.CLI;
|
||||
using Esiur.CLI.Client;
|
||||
using Esiur.CLI.Configuration;
|
||||
|
||||
namespace Esiur.CLI.Tests;
|
||||
|
||||
public sealed class ConfigurationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("ep://localhost", "ep://localhost")]
|
||||
[InlineData("ep://example.test:9000/sys/service", "ep://example.test:9000")]
|
||||
public void EndpointParserExtractsConnectionEndpoint(string value, string expected) =>
|
||||
Assert.Equal(expected, EndpointParser.ConnectionEndpoint(value));
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://localhost")]
|
||||
[InlineData("ep:///missing-host")]
|
||||
[InlineData("not-an-endpoint")]
|
||||
public void EndpointParserRejectsInvalidEndpoints(string value) =>
|
||||
Assert.Throws<CliException>(() => ConfigurationResolver.ValidateEndpoint(value));
|
||||
|
||||
[Theory]
|
||||
[InlineData("500ms", 500)]
|
||||
[InlineData("30s", 30_000)]
|
||||
[InlineData("2m", 120_000)]
|
||||
public void DurationParserSupportsDocumentedSuffixes(string value, double milliseconds) =>
|
||||
Assert.Equal(milliseconds, DurationParser.Parse(value).TotalMilliseconds);
|
||||
|
||||
[Fact]
|
||||
public async Task ConfigurationRoundTripsProfilesWithoutSecrets()
|
||||
{
|
||||
var directory = Directory.CreateTempSubdirectory("esiur-cli-test-");
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(directory.FullName, "config.json");
|
||||
var store = new ConfigurationStore(path);
|
||||
await store.SaveAsync(new CliConfiguration
|
||||
{
|
||||
DefaultProfile = "production",
|
||||
Profiles = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["production"] = new ConnectionProfile
|
||||
{
|
||||
Name = "production",
|
||||
Endpoint = "ep://host",
|
||||
Provider = "password",
|
||||
Identity = "ahmed",
|
||||
},
|
||||
},
|
||||
}, default);
|
||||
|
||||
var text = await File.ReadAllTextAsync(path);
|
||||
Assert.DoesNotContain("\"password\":", text, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("secret", text, StringComparison.OrdinalIgnoreCase);
|
||||
var loaded = await store.LoadAsync(default);
|
||||
Assert.Equal("production", loaded.DefaultProfile);
|
||||
Assert.Equal("ep://host", loaded.Profiles["PRODUCTION"].Endpoint);
|
||||
}
|
||||
finally { directory.Delete(true); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExplicitOptionsOverrideSelectedProfileAndGlobals()
|
||||
{
|
||||
var configuration = new CliConfiguration
|
||||
{
|
||||
DefaultProfile = "saved",
|
||||
OutputFormat = "table",
|
||||
Profiles = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["saved"] = new ConnectionProfile
|
||||
{
|
||||
Name = "saved", Endpoint = "ep://saved", OutputFormat = "raw",
|
||||
Identity = "stored",
|
||||
},
|
||||
},
|
||||
};
|
||||
var result = ConfigurationResolver.Resolve(configuration,
|
||||
new GlobalOptions("saved", "ep://explicit", null, "explicit", "json", TimeSpan.FromSeconds(4), false, false));
|
||||
Assert.Equal("ep://explicit", result.Endpoint);
|
||||
Assert.Equal("explicit", result.Identity);
|
||||
Assert.Equal("json", result.OutputFormat);
|
||||
Assert.Equal(TimeSpan.FromSeconds(4), result.Timeout);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("sys/service", "sys/service")]
|
||||
[InlineData("/sys/service/", "sys/service")]
|
||||
public void ResourcePathsAreNormalized(string value, string expected) =>
|
||||
Assert.Equal(expected, ResourceInspectionService.NormalizePath(value));
|
||||
|
||||
[Fact]
|
||||
public void SecretsAreRedactedFromDiagnostics()
|
||||
{
|
||||
var value = CliApplication.RedactSecrets("password=secret token: abc123");
|
||||
Assert.DoesNotContain("secret", value);
|
||||
Assert.DoesNotContain("abc123", value);
|
||||
Assert.Equal("password=*** token: ***", value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Tools\Esiur.CLI\Esiur.CLI.csproj" />
|
||||
<ProjectReference Include="..\..\Libraries\Esiur\Esiur.csproj" OutputItemType="Analyzer" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Esiur.CLI.Authentication;
|
||||
using Esiur.CLI.Client;
|
||||
using Esiur.CLI.Configuration;
|
||||
using Esiur.Protocol;
|
||||
using Esiur.Resource;
|
||||
using Esiur.Stores;
|
||||
|
||||
namespace Esiur.CLI.Tests;
|
||||
|
||||
[Resource]
|
||||
public partial class CliTestResource
|
||||
{
|
||||
[Export] public string Name { get; set; } = "Main";
|
||||
[Export, ReadOnly] public bool Running { get; set; } = true;
|
||||
}
|
||||
|
||||
public sealed class ReadOnlyIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task QueryDescribeAndGetWorkAgainstInProcessServer()
|
||||
{
|
||||
var server = new Warehouse();
|
||||
EsiurSession? session = null;
|
||||
try
|
||||
{
|
||||
var port = FindAvailablePort();
|
||||
await server.Put("sys", new MemoryStore());
|
||||
await server.Put("sys/server", new EpServer { Port = port, AllowUnauthorizedAccess = true });
|
||||
await server.Put("sys/service", new CliTestResource());
|
||||
await server.Open();
|
||||
|
||||
var credentials = new PromptCredentialService();
|
||||
var factory = new EsiurSessionFactory(credentials);
|
||||
var settings = new ResolvedConnection(
|
||||
"test", $"ep://localhost:{port}", null, null, null,
|
||||
"json", TimeSpan.FromSeconds(10), null);
|
||||
session = await factory.ConnectAsync(
|
||||
settings, false, TextReader.Null, TextWriter.Null, default);
|
||||
var service = new ResourceInspectionService();
|
||||
|
||||
var query = await service.QueryAsync(session, "sys", 1, null, default);
|
||||
Assert.Contains(query, item => item.Path == "sys/service" && item.Type.Contains(nameof(CliTestResource)));
|
||||
|
||||
var description = await service.DescribeAsync(session, "sys/service", true, default);
|
||||
Assert.Equal("sys/service", description.Path);
|
||||
Assert.Contains(description.Properties, item => item.Name == nameof(CliTestResource.Name) && Equals(item.Value, "Main"));
|
||||
|
||||
var values = await service.GetAsync(session, "sys/service", new[] { "Name", "1" }, default);
|
||||
Assert.Equal("Main", values[0].Value);
|
||||
Assert.Equal(true, values[1].Value);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (session is not null) await session.DisposeAsync();
|
||||
try { await server.Close(); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
static ushort FindAvailablePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = (ushort)((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Text.Json;
|
||||
using Esiur.CLI;
|
||||
using Esiur.CLI.Client;
|
||||
using Esiur.CLI.Rendering;
|
||||
|
||||
namespace Esiur.CLI.Tests;
|
||||
|
||||
public sealed class RenderingTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task JsonOutputUsesStableCamelCaseShape()
|
||||
{
|
||||
var writer = new StringWriter();
|
||||
await new OutputRenderer(writer).RenderAsync(
|
||||
new[] { new PropertyResult("sys/service", "Name", 0, "Main") },
|
||||
OutputFormat.Json, default);
|
||||
using var document = JsonDocument.Parse(writer.ToString());
|
||||
var value = document.RootElement[0];
|
||||
Assert.Equal("sys/service", value.GetProperty("resource").GetString());
|
||||
Assert.Equal("Main", value.GetProperty("value").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RawOutputPrintsOnlyPropertyValues()
|
||||
{
|
||||
var writer = new StringWriter();
|
||||
await new OutputRenderer(writer).RenderAsync(
|
||||
new[]
|
||||
{
|
||||
new PropertyResult("sys/service", "Name", 0, "Main"),
|
||||
new PropertyResult("sys/service", "Running", 1, true),
|
||||
}, OutputFormat.Raw, default);
|
||||
Assert.Equal($"Main{Environment.NewLine}true{Environment.NewLine}", writer.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TableOutputContainsHeadersAndRows()
|
||||
{
|
||||
var writer = new StringWriter();
|
||||
await new OutputRenderer(writer).RenderAsync(
|
||||
new[] { new ResourceSummary("service", "sys/service", 3, "Demo.Service", "0x01", 0) },
|
||||
OutputFormat.Table, default);
|
||||
Assert.Contains("Session Id", writer.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("sys/service", writer.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("table", OutputFormat.Table)]
|
||||
[InlineData("json", OutputFormat.Json)]
|
||||
[InlineData("jsonl", OutputFormat.JsonLines)]
|
||||
[InlineData("raw", OutputFormat.Raw)]
|
||||
public void OutputFormatsParse(string value, OutputFormat expected) =>
|
||||
Assert.Equal(expected, OutputFormatExtensions.Parse(value));
|
||||
}
|
||||
Reference in New Issue
Block a user