Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace TALXIS.CLI.Features.Environment;
Name = "environment",
Alias = "env",
Description = "Manage the footprint of your project in a live target environment (packages, solutions, deployment history).",
Children = new[] { typeof(Package.PackageCliCommand), typeof(Solution.SolutionCliCommand), typeof(Deployment.DeploymentCliCommand), typeof(Data.EnvDataCliCommand), typeof(Entity.EntityCliCommand), typeof(OptionSet.OptionSetCliCommand), typeof(Setting.SettingCliCommand), typeof(Changeset.ChangesetCliCommand), typeof(Component.ComponentCliCommand), typeof(Publisher.PublisherCliCommand) },
Children = new[] { typeof(EnvironmentListCliCommand), typeof(Package.PackageCliCommand), typeof(Solution.SolutionCliCommand), typeof(Deployment.DeploymentCliCommand), typeof(Data.EnvDataCliCommand), typeof(Entity.EntityCliCommand), typeof(OptionSet.OptionSetCliCommand), typeof(Setting.SettingCliCommand), typeof(Changeset.ChangesetCliCommand), typeof(Component.ComponentCliCommand), typeof(Publisher.PublisherCliCommand) },
ShortFormAutoGenerate = CliNameAutoGenerate.None
)]
public class EnvironmentCliCommand
Expand Down
108 changes: 108 additions & 0 deletions src/TALXIS.CLI.Features.Environment/EnvironmentListCliCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using DotMake.CommandLine;
using Microsoft.Extensions.Logging;
using TALXIS.CLI.Core;
using TALXIS.CLI.Core.Abstractions;
using TALXIS.CLI.Core.DependencyInjection;
using TALXIS.CLI.Core.Model;
using TALXIS.CLI.Logging;
using TALXIS.CLI.Platform.PowerPlatform.Control;

namespace TALXIS.CLI.Features.Environment;

[CliReadOnly]
[CliCommand(
Name = "list",
Description = "List Dataverse environments you can access."
)]
public class EnvironmentListCliCommand : TxcLeafCommand
{
protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(nameof(EnvironmentListCliCommand));

[CliOption(
Name = "--credential",
Aliases = ["-c"],
Description = "Credential alias used to query environments. Defaults to the only stored credential; required when more than one exists.",
Required = false
)]
public string? Credential { get; set; }

protected override async Task<int> ExecuteAsync()
{
var store = TxcServices.Get<ICredentialStore>();
var credential = await ResolveCredentialAsync(store).ConfigureAwait(false);
if (credential is null) return ExitValidationError;

var connection = new Connection
{
Id = "(discovery)",
Provider = ProviderKind.Dataverse,
Cloud = credential.Cloud ?? CloudInstance.Public,
TenantId = credential.TenantId,
};

var catalog = TxcServices.Get<IPowerPlatformEnvironmentCatalog>();
var environments = await catalog.ListAsync(connection, credential, CancellationToken.None).ConfigureAwait(false);

var rows = environments
.OrderBy(e => e.DisplayName, StringComparer.OrdinalIgnoreCase)
.Select(e => new EnvRow(
e.DisplayName,
e.EnvironmentUrl.ToString(),
e.EnvironmentType?.ToString(),
e.EnvironmentId,
e.OrganizationId,
credential.TenantId))
.ToList();

OutputFormatter.WriteList(rows, EnvironmentListPrinter.PrintTable);
return ExitSuccess;
}

private async Task<Credential?> ResolveCredentialAsync(ICredentialStore store)
{
if (!string.IsNullOrWhiteSpace(Credential))
{
var alias = Credential.Trim();
var match = await store.GetAsync(alias, CancellationToken.None).ConfigureAwait(false);
if (match is null) Logger.LogError("Credential '{Alias}' was not found. Run 'txc config auth list' to see stored credentials.", alias);
return match;
}

var all = await store.ListAsync(CancellationToken.None).ConfigureAwait(false);
if (all.Count == 0)
{
Logger.LogError("No stored credentials. Sign in first with 'txc config auth login', then retry.");
return null;
}
if (all.Count > 1)
{
var aliases = string.Join(", ", all.Select(c => c.Id).OrderBy(s => s, StringComparer.OrdinalIgnoreCase));
Logger.LogError("Several credentials are stored ({Aliases}). Pick one with --credential <alias>.", aliases);
return null;
}
return all[0];
}
}

internal sealed record EnvRow(string Name, string Url, string? Type, Guid EnvironmentId, Guid? OrganizationId, string? TenantId);

internal static class EnvironmentListPrinter
{
public static void PrintTable(IReadOnlyList<EnvRow> rows)
{
if (rows.Count == 0)
{
OutputWriter.WriteLine("No environments found.");
return;
}

OutputWriter.WriteLine($" {"NAME",-32} {"TYPE",-12} URL");
foreach (var r in rows)
{
OutputWriter.WriteLine($" {Trim(r.Name, 32),-32} {r.Type ?? "-",-12} {r.Url}");
}
}

private static string Trim(string value, int max)
=> value.Length <= max ? value : value[..(max - 1)] + "...";
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<ItemGroup>
<ProjectReference Include="..\TALXIS.CLI.Core\TALXIS.CLI.Core.csproj" />
<ProjectReference Include="..\TALXIS.CLI.Logging\TALXIS.CLI.Logging.csproj" />
<ProjectReference Include="..\TALXIS.CLI.Platform.PowerPlatform.Control\TALXIS.CLI.Platform.PowerPlatform.Control.csproj" />
</ItemGroup>

</Project>
95 changes: 95 additions & 0 deletions tests/TALXIS.CLI.Tests/Environment/EnvironmentListCommandTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using Microsoft.Extensions.DependencyInjection;
using TALXIS.CLI.Core.Abstractions;
using TALXIS.CLI.Core.Model;
using TALXIS.CLI.Features.Environment;
using TALXIS.CLI.Platform.PowerPlatform.Control;
using TALXIS.CLI.Tests.Config.Commands;
using Xunit;

namespace TALXIS.CLI.Tests.Environment;

[Collection("Sequential")]
public class EnvironmentListCommandTests
{
private static async Task SeedCredentialAsync(CommandTestHost host, string id)
{
var store = host.Provider.GetRequiredService<ICredentialStore>();
await store.UpsertAsync(new Credential
{
Id = id,
Kind = CredentialKind.InteractiveBrowser,
TenantId = "tenant-guid",
Cloud = CloudInstance.Public,
}, default);
}

private static void SeedEnvironment(CommandTestHost host, string name, string url)
{
host.EnvironmentCatalog.Add(new PowerPlatformEnvironmentSummary(
EnvironmentId: Guid.NewGuid(),
DisplayName: name,
EnvironmentUrl: new Uri(url),
UniqueName: null,
DomainName: null,
OrganizationId: Guid.NewGuid(),
EnvironmentType: EnvironmentType.Sandbox));
}

[Fact]
public async Task List_SingleCredential_ReturnsSuccess()
{
using var host = new CommandTestHost();
await SeedCredentialAsync(host, "only-cred");
SeedEnvironment(host, "Dev", "https://dev.crm4.dynamics.com/");

var exit = await new EnvironmentListCliCommand().RunAsync();

Assert.Equal(0, exit);
}

[Fact]
public async Task List_NoCredentials_ReturnsValidationError()
{
using var host = new CommandTestHost();

var exit = await new EnvironmentListCliCommand().RunAsync();

Assert.Equal(2, exit);
}

[Fact]
public async Task List_MultipleCredentialsWithoutFlag_ReturnsValidationError()
{
using var host = new CommandTestHost();
await SeedCredentialAsync(host, "cred-a");
await SeedCredentialAsync(host, "cred-b");

var exit = await new EnvironmentListCliCommand().RunAsync();

Assert.Equal(2, exit);
}

[Fact]
public async Task List_ExplicitCredential_ReturnsSuccess()
{
using var host = new CommandTestHost();
await SeedCredentialAsync(host, "cred-a");
await SeedCredentialAsync(host, "cred-b");
SeedEnvironment(host, "Dev", "https://dev.crm4.dynamics.com/");

var exit = await new EnvironmentListCliCommand { Credential = "cred-b" }.RunAsync();

Assert.Equal(0, exit);
}

[Fact]
public async Task List_UnknownCredential_ReturnsValidationError()
{
using var host = new CommandTestHost();
await SeedCredentialAsync(host, "cred-a");

var exit = await new EnvironmentListCliCommand { Credential = "does-not-exist" }.RunAsync();

Assert.Equal(2, exit);
}
}