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 @@ -136,49 +136,39 @@ private sealed record BindingNames(
? null
: await FindExistingProfileAsync(existingConnection.Id, acquired.Credential.Id, ct).ConfigureAwait(false);

if (!string.IsNullOrEmpty(explicitName))
return new BindingNames(explicitName, existingConnection?.Id ?? explicitName);
// Resolve environment metadata on every path (not only when deriving
// a name) so EnvironmentType reaches the connection and the
// destructive-operation guard can tell dev/test apart from production.
var environment = await TryGetEnvironmentAsync(request, environmentUrl, cloud, acquired, ct).ConfigureAwait(false);

if (existingProfile is not null)
return new BindingNames(existingProfile.Id, existingProfile.ConnectionRef!);
// Fall back to metadata already stored on the connection so a failed
// catalog lookup does not wipe values a previous refresh persisted.
var resolvedEnvironmentId = environment?.EnvironmentId ?? existingConnection?.EnvironmentId;
var resolvedDisplayName = environment?.DisplayName ?? existingConnection?.DisplayName;
var resolvedEnvironmentType = environment?.EnvironmentType ?? existingConnection?.EnvironmentType;
var resolvedOrganizationId = environment?.OrganizationId;
if (resolvedOrganizationId is null && Guid.TryParse(existingConnection?.OrganizationId, out var existingOrgId))
resolvedOrganizationId = existingOrgId;

string? preferredBase = null;
Guid? resolvedEnvironmentId = null;
string? resolvedDisplayName = null;
EnvironmentType? resolvedEnvironmentType = null;
Guid? resolvedOrganizationId = null;
var ephemeralConnection = new Connection
if (!string.IsNullOrEmpty(explicitName))
{
Id = "(ephemeral)",
Provider = request.Provider,
EnvironmentUrl = environmentUrl.ToString().TrimEnd('/'),
Cloud = cloud,
TenantId = request.TenantId ?? acquired.TenantId,
};
return new BindingNames(explicitName, existingConnection?.Id ?? explicitName,
resolvedEnvironmentId, resolvedDisplayName, resolvedEnvironmentType, resolvedOrganizationId);
}

try
if (existingProfile is not null)
{
var environment = await _environmentCatalog
.TryGetByEnvironmentUrlAsync(ephemeralConnection, acquired.Credential, environmentUrl, ct)
.ConfigureAwait(false);
if (environment is not null)
{
// Include tenant domain in the slug so multi-customer users can
// distinguish environments across tenants at a glance.
var tenantDomain = CredentialAliasResolver.ExtractTenantShortName(acquired.Upn);
preferredBase = ProviderUrlResolver.DeriveDefaultName(environment.DisplayName, request.EnvironmentUrl, tenantDomain);
resolvedEnvironmentId = environment.EnvironmentId;
resolvedDisplayName = environment.DisplayName;
resolvedEnvironmentType = environment.EnvironmentType;
resolvedOrganizationId = environment.OrganizationId;
}
return new BindingNames(existingProfile.Id, existingProfile.ConnectionRef!,
resolvedEnvironmentId, resolvedDisplayName, resolvedEnvironmentType, resolvedOrganizationId);
}
catch (Exception ex) when (ex is InvalidOperationException or NotSupportedException)

string? preferredBase = null;
if (environment is not null)
{
_logger.LogWarning(
ex,
"Could not resolve Power Platform environment metadata for '{Url}'. Falling back to the URL host.",
request.EnvironmentUrl);
// Include tenant domain in the slug so multi-customer users can
// distinguish environments across tenants at a glance.
var tenantDomain = CredentialAliasResolver.ExtractTenantShortName(acquired.Upn);
preferredBase = ProviderUrlResolver.DeriveDefaultName(environment.DisplayName, request.EnvironmentUrl, tenantDomain);
}

if (string.IsNullOrEmpty(preferredBase))
Expand Down Expand Up @@ -214,6 +204,38 @@ await _profiles.GetAsync(candidate, existsCt).ConfigureAwait(false) is not null
resolvedDisplayName, resolvedEnvironmentType, resolvedOrganizationId);
}

private async Task<PowerPlatformEnvironmentSummary?> TryGetEnvironmentAsync(
ProfileBootstrapRequest request,
Uri environmentUrl,
CloudInstance cloud,
InteractiveCredentialResult acquired,
CancellationToken ct)
{
var ephemeralConnection = new Connection
{
Id = "(ephemeral)",
Provider = request.Provider,
EnvironmentUrl = environmentUrl.ToString().TrimEnd('/'),
Cloud = cloud,
TenantId = request.TenantId ?? acquired.TenantId,
};

try
{
return await _environmentCatalog
.TryGetByEnvironmentUrlAsync(ephemeralConnection, acquired.Credential, environmentUrl, ct)
.ConfigureAwait(false);
}
catch (Exception ex) when (ex is InvalidOperationException or NotSupportedException)
{
_logger.LogWarning(
ex,
"Could not resolve Power Platform environment metadata for '{Url}'. Falling back to the URL host.",
request.EnvironmentUrl);
return null;
}
}

private async Task<Connection?> FindExistingConnectionAsync(
ProfileBootstrapRequest request,
CloudInstance cloud,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,76 @@ public async Task UrlMode_ExplicitName_OverridesDerived()
Assert.NotNull(await connections.GetAsync("my-profile", default));
}

[Fact]
public async Task UrlMode_ExplicitName_StillResolvesEnvironmentMetadata()
{
var catalog = new CommandTestHost.FakePowerPlatformEnvironmentCatalog();
catalog.Add(new PowerPlatformEnvironmentSummary(
EnvironmentId: Guid.Parse("11111111-2222-3333-4444-555555555555"),
DisplayName: "Contoso DevBox",
EnvironmentUrl: new Uri("https://devbox.crm4.dynamics.com/"),
UniqueName: "contoso-devbox",
DomainName: "devbox",
OrganizationId: Guid.Parse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"),
EnvironmentType: EnvironmentType.Developer));

using var host = new CommandTestHost(environmentCatalog: catalog);
using (OutputWriter.RedirectTo(new StringWriter()))
{
var exit = await new ProfileCreateCliCommand
{
Url = "https://devbox.crm4.dynamics.com/",
Name = "devbox-2896",
}.RunAsync();
Assert.Equal(0, exit);
}

var connections = (IConnectionStore)host.Provider.GetService(typeof(IConnectionStore))!;
var connection = await connections.GetAsync("devbox-2896", default);
Assert.NotNull(connection);
Assert.Equal(EnvironmentType.Developer, connection!.EnvironmentType);
Assert.Equal("Contoso DevBox", connection.DisplayName);
Assert.Equal(Guid.Parse("11111111-2222-3333-4444-555555555555"), connection.EnvironmentId);
Assert.Equal("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", connection.OrganizationId);
}

[Fact]
public async Task UrlMode_Recreate_WhenLookupFails_PreservesExistingMetadata()
{
var catalog = new CommandTestHost.FakePowerPlatformEnvironmentCatalog();
catalog.Add(new PowerPlatformEnvironmentSummary(
EnvironmentId: Guid.Parse("11111111-2222-3333-4444-555555555555"),
DisplayName: "Contoso DevBox",
EnvironmentUrl: new Uri("https://devbox.crm4.dynamics.com/"),
UniqueName: "contoso-devbox",
DomainName: "devbox",
OrganizationId: Guid.Parse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"),
EnvironmentType: EnvironmentType.Developer));

using var host = new CommandTestHost(environmentCatalog: catalog);
using (OutputWriter.RedirectTo(new StringWriter()))
{
Assert.Equal(0, await new ProfileCreateCliCommand
{
Url = "https://devbox.crm4.dynamics.com/",
Name = "devbox-2896",
}.RunAsync());

catalog.Failure = new InvalidOperationException("admin lookup failed");
Assert.Equal(0, await new ProfileCreateCliCommand
{
Url = "https://devbox.crm4.dynamics.com/",
Name = "devbox-2896",
}.RunAsync());
}

var connections = (IConnectionStore)host.Provider.GetService(typeof(IConnectionStore))!;
var connection = await connections.GetAsync("devbox-2896", default);
Assert.NotNull(connection);
Assert.Equal(EnvironmentType.Developer, connection!.EnvironmentType);
Assert.Equal("Contoso DevBox", connection.DisplayName);
}

[Fact]
public async Task UrlMode_InfersSovereignCloud_FromEnvironmentUrl_WhenCloudOmitted()
{
Expand Down