Skip to content
41 changes: 17 additions & 24 deletions src/Commands/AccumulatedCost/AccumulatedCostCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ public AccumulatedCostCommand(ICostRetriever costRetriever)

public override ValidationResult Validate(CommandContext context, AccumulatedCostSettings settings)
{
// Check if we have any scope parameters when the scope requires subscription
if (settings.GetScope.IsSubscriptionBased && !settings.Subscription.HasValue)
{
// Try to get subscription from Azure CLI
try
{
var subscriptionId = Guid.Parse(AzCommand.GetDefaultAzureSubscriptionId());
settings.Subscription = subscriptionId;
Comment on lines +33 to +37
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Validate method modifies the settings object by setting the Subscription property. This violates the principle that validation should not have side effects. If validation fails later or if the command is not executed, the settings object is left in a modified state. Consider moving the subscription retrieval logic to the beginning of ExecuteAsync, or return the retrieved subscription ID as part of a validation context without modifying the settings.

Suggested change
// Try to get subscription from Azure CLI
try
{
var subscriptionId = Guid.Parse(AzCommand.GetDefaultAzureSubscriptionId());
settings.Subscription = subscriptionId;
// Try to get subscription from Azure CLI and ensure it is valid
try
{
_ = Guid.Parse(AzCommand.GetDefaultAzureSubscriptionId());

Copilot uses AI. Check for mistakes.
}
catch (Exception)
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catching all exceptions with a generic Exception catch block discards valuable diagnostic information. This makes it difficult to distinguish between different failure scenarios (e.g., Azure CLI not installed vs. not logged in vs. parse errors). Consider catching specific exceptions and providing more targeted error messages, or at least logging the exception details when Debug mode is enabled.

Copilot uses AI. Check for mistakes.
{
Comment on lines +36 to +40
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous implementation included debug logging when retrieving the subscription ID from Azure CLI. This helpful debugging information has been removed in the refactoring. Consider adding similar debug logging when settings.Debug is true to help users troubleshoot subscription retrieval issues.

Suggested change
var subscriptionId = Guid.Parse(AzCommand.GetDefaultAzureSubscriptionId());
settings.Subscription = subscriptionId;
}
catch (Exception)
{
if (settings.Debug)
{
AnsiConsole.MarkupLine("[grey]Attempting to retrieve default Azure subscription ID from Azure CLI...[/]");
}
var subscriptionId = Guid.Parse(AzCommand.GetDefaultAzureSubscriptionId());
if (settings.Debug)
{
AnsiConsole.MarkupLine($"[grey]Retrieved subscription ID '{subscriptionId}' from Azure CLI.[/]");
}
settings.Subscription = subscriptionId;
}
catch (Exception ex)
{
if (settings.Debug)
{
AnsiConsole.MarkupLine("[grey]Failed to retrieve subscription ID from Azure CLI.[/]");
AnsiConsole.WriteException(ex);
}

Copilot uses AI. Check for mistakes.
// If we can't get the subscription from Azure CLI, return an error
return ValidationResult.Error("No subscription ID provided and unable to retrieve from Azure CLI. Please specify a subscription ID using -s or --subscription, or login to Azure CLI using 'az login'. Use --help for more information.");
}
}

// Automatically set timeframe to Custom if both from and to dates are provided
settings.ApplyAutoTimeframe();

Expand All @@ -51,32 +67,9 @@ public override async Task<int> ExecuteAsync(CommandContext context, Accumulated
_costRetriever.CostApiAddress = settings.CostApiAddress;
_costRetriever.HttpTimeout = TimeSpan.FromSeconds(settings.HttpTimeout);

// Get the subscription ID from the settings
// Get the subscription ID from the settings (already validated and set in Validate method)
var subscriptionId = settings.Subscription;

if (subscriptionId.HasValue == false && (settings.GetScope.IsSubscriptionBased))
{
// Get the subscription ID from the Azure CLI
try
{
if (settings.Debug)
AnsiConsole.WriteLine("No subscription ID specified. Trying to retrieve the default subscription ID from Azure CLI.");

subscriptionId = Guid.Parse(AzCommand.GetDefaultAzureSubscriptionId());

if (settings.Debug)
AnsiConsole.WriteLine($"Default subscription ID retrieved from az cli: {subscriptionId}");

settings.Subscription = subscriptionId;
}
catch (Exception e)
{
AnsiConsole.WriteException(new ArgumentException(
"Missing subscription ID. Please specify a subscription ID or login to Azure CLI.", e));
return -1;
}
}

AccumulatedCostDetails accumulatedCost = null;

Subscription subscription = null;
Expand Down
62 changes: 62 additions & 0 deletions tests/AzureCostCli.Tests/Commands/AdditionalCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public void Validate_WithCustomTimeframeAndValidDates_ReturnsSuccess()
// Arrange
var settings = new AccumulatedCostSettings
{
Subscription = Guid.NewGuid(),
Timeframe = TimeframeType.Custom,
From = new DateOnly(2023, 1, 1),
To = new DateOnly(2023, 1, 31)
Expand All @@ -45,6 +46,7 @@ public void Validate_WithCustomTimeframeAndFromDateAfterToDate_ReturnsError()
// Arrange
var settings = new AccumulatedCostSettings
{
Subscription = Guid.NewGuid(),
Timeframe = TimeframeType.Custom,
From = new DateOnly(2023, 1, 31),
To = new DateOnly(2023, 1, 1)
Expand All @@ -65,6 +67,7 @@ public void Validate_WithNonCustomTimeframe_ReturnsSuccess()
// Arrange
var settings = new AccumulatedCostSettings
{
Subscription = Guid.NewGuid(),
Timeframe = TimeframeType.MonthToDate
};
var context = CreateCommandContext();
Expand All @@ -82,6 +85,7 @@ public void Validate_WithBothFromAndToDates_AutomaticallySetsTimeframeToCustom()
// Arrange
var settings = new AccumulatedCostSettings
{
Subscription = Guid.NewGuid(),
Timeframe = TimeframeType.BillingMonthToDate, // Not Custom initially
From = new DateOnly(2023, 1, 1),
To = new DateOnly(2023, 1, 31)
Expand All @@ -102,6 +106,7 @@ public void Validate_WithOnlyFromDate_DoesNotSetTimeframeToCustom()
// Arrange
var settings = new AccumulatedCostSettings
{
Subscription = Guid.NewGuid(),
Timeframe = TimeframeType.BillingMonthToDate,
From = new DateOnly(2023, 1, 1)
// To is not set
Expand All @@ -122,6 +127,7 @@ public void Validate_WithAutoCustomAndInvalidDates_ReturnsError()
// Arrange
var settings = new AccumulatedCostSettings
{
Subscription = Guid.NewGuid(),
Timeframe = TimeframeType.BillingMonthToDate, // Will be auto-set to Custom
From = new DateOnly(2023, 1, 31), // From after To
To = new DateOnly(2023, 1, 1)
Expand All @@ -144,6 +150,62 @@ public void Constructor_SetsUpOutputFormatters()
command.ShouldNotBeNull();
}

[Fact]
public void Validate_WithNoSubscriptionAndNoAzureCLI_ReturnsError()
{
// Arrange
var settings = new AccumulatedCostSettings
{
Subscription = null // No subscription provided
};
var context = CreateCommandContext();

// Act
var result = _command.Validate(context, settings);

// Assert
result.Successful.ShouldBeFalse();
result.Message.ShouldContain("No subscription ID provided");
result.Message.ShouldContain("--help");
}
Comment on lines +153 to +170
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test Validate_WithNoSubscriptionAndNoAzureCLI_ReturnsError depends on the runtime environment (whether Azure CLI is installed and accessible). This makes the test non-deterministic and could fail in CI/CD environments where Azure CLI is installed. Consider refactoring AzCommand.GetDefaultAzureSubscriptionId to use dependency injection or making it mockable to ensure tests are isolated from the environment.

Copilot uses AI. Check for mistakes.

[Fact]
public void Validate_WithSubscription_ReturnsSuccess()
{
// Arrange
var settings = new AccumulatedCostSettings
{
Subscription = Guid.NewGuid(),
Timeframe = TimeframeType.BillingMonthToDate
};
var context = CreateCommandContext();

// Act
var result = _command.Validate(context, settings);

// Assert
result.Successful.ShouldBeTrue();
}

[Fact]
public void Validate_WithBillingAccountAndNoSubscription_ReturnsSuccess()
{
// Arrange
var settings = new AccumulatedCostSettings
{
Subscription = null,
BillingAccountId = "12345",
Timeframe = TimeframeType.BillingMonthToDate
};
var context = CreateCommandContext();

// Act
var result = _command.Validate(context, settings);

// Assert
result.Successful.ShouldBeTrue();
}

private static CommandContext CreateCommandContext()
{
var remainingArguments = Mock.Of<IRemainingArguments>();
Expand Down
Loading