Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Refactor HandleUnresolvedParametersAsync for improved testing and int…
…eraction handling
  • Loading branch information
davidfowl committed Jul 3, 2025
commit c7f95fc301882daf72f46d98822613b23e270e05
19 changes: 13 additions & 6 deletions src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,17 @@ await _notificationService.PublishUpdateAsync(parameterResource, s =>
}
}

public async Task HandleUnresolvedParametersAsync()
// Internal for testing purposes.
private async Task HandleUnresolvedParametersAsync()
{
await HandleUnresolvedParametersAsync(_unresolvedParameters).ConfigureAwait(false);
}

// Internal for testing purposes - allows passing specific parameters to test.
internal async Task HandleUnresolvedParametersAsync(IList<ParameterResource> unresolvedParameters)
{
// This method will continue in a loop until all unresolved parameters are resolved.
while (_unresolvedParameters.Count > 0)
while (unresolvedParameters.Count > 0)
{
// First we show a notification that there are unresolved parameters.
var result = await interactionService.PromptMessageBarAsync(
Expand All @@ -135,7 +142,7 @@ public async Task HandleUnresolvedParametersAsync()
// Now we build up a new form base on the unresolved parameters.
var inputs = new List<InteractionInput>();

foreach (var parameter in _unresolvedParameters)
foreach (var parameter in unresolvedParameters)
{
// Create an input for each unresolved parameter.
inputs.Add(new InteractionInput
Expand All @@ -161,9 +168,9 @@ public async Task HandleUnresolvedParametersAsync()
if (!valuesPrompt.Canceled)
{
// Iterate through the unresolved parameters and set their values based on user input.
for (var i = _unresolvedParameters.Count - 1; i >= 0; i--)
for (var i = unresolvedParameters.Count - 1; i >= 0; i--)
{
var parameter = _unresolvedParameters[i];
var parameter = unresolvedParameters[i];
var inputValue = valuesPrompt.Data[i].Value;

if (inputValue is null)
Expand All @@ -186,7 +193,7 @@ await _notificationService.PublishUpdateAsync(parameter, s =>
.ConfigureAwait(false);

// Remove the parameter from unresolved parameters list.
_unresolvedParameters.RemoveAt(i);
unresolvedParameters.RemoveAt(i);
}
}
}
Expand Down
149 changes: 130 additions & 19 deletions tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,30 @@ public async Task InitializeParametersAsync_WithValidParameters_SetsActiveState(
}
}

[Fact]
public async Task InitializeParametersAsync_WithValidParametersAndDashboardEnabled_SetsActiveState()
{
// Arrange
var interactionService = CreateInteractionService(disableDashboard: false);
var parameterProcessor = CreateParameterProcessor(interactionService: interactionService, disableDashboard: false);
var parameters = new[]
{
CreateParameterResource("param1", "value1"),
CreateParameterResource("param2", "value2")
};

// Act
await parameterProcessor.InitializeParametersAsync(parameters);

// Assert
foreach (var param in parameters)
{
Assert.NotNull(param.WaitForValueTcs);
Assert.True(param.WaitForValueTcs.Task.IsCompletedSuccessfully);
Assert.Equal(param.Value, await param.WaitForValueTcs.Task);
}
}

[Fact]
public async Task InitializeParametersAsync_WithSecretParameter_MarksAsSecret()
{
Expand All @@ -63,9 +87,8 @@ public async Task InitializeParametersAsync_WithSecretParameter_MarksAsSecret()
await watchTask.WaitAsync(TimeSpan.FromSeconds(5));

// Assert
Assert.Single(updates);
var (resource, snapshot) = updates[0];
Assert.Equal(secretParam, resource);
var (resource, snapshot) = Assert.Single(updates);
Assert.Same(secretParam, resource);
Assert.Equal(KnownResourceStates.Active, snapshot.State?.Text);
Assert.Equal(KnownResourceStateStyles.Success, snapshot.State?.Style);
}
Expand Down Expand Up @@ -104,6 +127,22 @@ public async Task InitializeParametersAsync_WithMissingParameterValue_SetsExcept
Assert.IsType<MissingParameterValueException>(parameterWithMissingValue.WaitForValueTcs.Task.Exception?.InnerException);
}

[Fact]
public async Task InitializeParametersAsync_WithMissingParameterValueAndDashboardEnabled_LeavesUnresolved()
{
// Arrange
var interactionService = CreateInteractionService(disableDashboard: false);
var parameterProcessor = CreateParameterProcessor(interactionService: interactionService, disableDashboard: false);
var parameterWithMissingValue = CreateParameterWithMissingValue("missingParam");

// Act
await parameterProcessor.InitializeParametersAsync([parameterWithMissingValue]);

// Assert - Parameter should remain unresolved when dashboard is enabled
Assert.NotNull(parameterWithMissingValue.WaitForValueTcs);
Assert.False(parameterWithMissingValue.WaitForValueTcs.Task.IsCompleted);
}

[Fact]
public async Task InitializeParametersAsync_WithNonMissingParameterException_SetsException()
{
Expand All @@ -122,48 +161,120 @@ public async Task InitializeParametersAsync_WithNonMissingParameterException_Set
}

[Fact]
public async Task HandleUnresolvedParametersAsync_WithInteractionService_DoesNotThrow()
public async Task HandleUnresolvedParametersAsync_WithMultipleUnresolvedParameters_CreatesInteractions()
{
// Arrange
var interactionService = CreateInteractionService();
var parameterProcessor = CreateParameterProcessor(interactionService: interactionService);
var param1 = CreateParameterWithMissingValue("param1");
var param2 = CreateParameterWithMissingValue("param2");
var secretParam = CreateParameterWithMissingValue("secretParam");

// Act - Initialize parameters to add them to unresolved list
await parameterProcessor.HandleUnresolvedParametersAsync([param1, param2, secretParam]);

// Assert - All parameters should be unresolved and interaction should be created
Assert.NotNull(param1.WaitForValueTcs);
Assert.NotNull(param2.WaitForValueTcs);
Assert.NotNull(secretParam.WaitForValueTcs);
Assert.False(param1.WaitForValueTcs.Task.IsCompleted);
Assert.False(param2.WaitForValueTcs.Task.IsCompleted);
Assert.False(secretParam.WaitForValueTcs.Task.IsCompleted);

// Verify there's an active interaction for the parameters
var interactions = interactionService.GetCurrentInteractions();
Assert.NotEmpty(interactions);

// Verify the interaction has the expected title
Assert.Contains(interactions, i => i.Title == "Unresolved Parameters");
}

[Fact]
public async Task HandleUnresolvedParametersAsync_WithNoInteractionService_DoesNotCreateInteractions()
{
// Arrange - Use interaction service with dashboard disabled
var interactionService = CreateInteractionService(disableDashboard: true);
var parameterProcessor = CreateParameterProcessor(interactionService: interactionService);
var parameterWithMissingValue = CreateParameterWithMissingValue("missingParam");

// Act - Initialize parameters first to add to unresolved list
// Act - Initialize parameters
await parameterProcessor.InitializeParametersAsync([parameterWithMissingValue]);

// Allow background task to start
await Task.Delay(50);
// Allow background task time to run (if it would run)
await Task.Delay(100);

// Assert - The background task should have started without throwing
// Assert - Parameter should be in error state since interaction service is not available
Assert.NotNull(parameterWithMissingValue.WaitForValueTcs);
// The parameter should remain unresolved since we don't complete the interaction
Assert.False(parameterWithMissingValue.WaitForValueTcs.Task.IsCompleted);
Assert.True(parameterWithMissingValue.WaitForValueTcs.Task.IsCompleted);
Assert.True(parameterWithMissingValue.WaitForValueTcs.Task.IsFaulted);

// Verify there's an active interaction for the parameter
// No interactions should be created
var interactions = interactionService.GetCurrentInteractions();
Assert.NotEmpty(interactions);
Assert.Empty(interactions);
}

[Fact]
public async Task HandleUnresolvedParametersAsync_CallDirectly_DoesNotThrow()
public async Task InitializeParametersAsync_WithEmptyParameterList_CompletesSuccessfully()
{
// Arrange
var parameterProcessor = CreateParameterProcessor();

// Act & Assert - Should not throw
await parameterProcessor.InitializeParametersAsync(Array.Empty<ParameterResource>());
}

[Fact]
public void HandleUnresolvedParametersAsync_WithSingleUnresolvedParameter_CreatesInteraction()
{
// Arrange
var interactionService = CreateInteractionService();
var parameterProcessor = CreateParameterProcessor(interactionService: interactionService);
var unresolvedParam = CreateParameterWithMissingValue("testParam");

// Initialize the parameter's WaitForValueTcs
unresolvedParam.WaitForValueTcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);

// Act - Directly call HandleUnresolvedParametersAsync with the parameter list
var handleTask = parameterProcessor.HandleUnresolvedParametersAsync([unresolvedParam]);

// Act & Assert - Should not throw even when called directly with no unresolved parameters
await parameterProcessor.HandleUnresolvedParametersAsync();
// Assert - Verify interaction was created (the method should start immediately and create interaction)
var interactions = interactionService.GetCurrentInteractions();
Assert.NotEmpty(interactions);
Assert.Contains(interactions, i => i.Title == "Unresolved Parameters");

// Verify the parameter is still unresolved (waiting for user input)
Assert.NotNull(unresolvedParam.WaitForValueTcs);
Assert.False(unresolvedParam.WaitForValueTcs.Task.IsCompleted);
}

[Fact]
public async Task InitializeParametersAsync_WithEmptyParameterList_CompletesSuccessfully()
public void HandleUnresolvedParametersAsync_WithMultipleUnresolvedParameters_CreatesCorrectInteraction()
{
// Arrange
var parameterProcessor = CreateParameterProcessor();
var interactionService = CreateInteractionService();
var parameterProcessor = CreateParameterProcessor(interactionService: interactionService);
var unresolvedParam1 = CreateParameterWithMissingValue("param1");
var unresolvedParam2 = CreateParameterWithMissingValue("param2");

// Act & Assert - Should not throw
await parameterProcessor.InitializeParametersAsync(Array.Empty<ParameterResource>());
// Initialize the parameters' WaitForValueTcs
unresolvedParam1.WaitForValueTcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
unresolvedParam2.WaitForValueTcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);

// Act - Directly call HandleUnresolvedParametersAsync with the parameter list
var handleTask = parameterProcessor.HandleUnresolvedParametersAsync([unresolvedParam1, unresolvedParam2]);

// Assert - Verify interaction was created for multiple parameters
var interactions = interactionService.GetCurrentInteractions();
Assert.NotEmpty(interactions);

var parameterInteraction = interactions.FirstOrDefault(i => i.Title == "Unresolved Parameters");
Assert.NotNull(parameterInteraction);

// Verify both parameters are still unresolved (waiting for user input)
Assert.NotNull(unresolvedParam1.WaitForValueTcs);
Assert.NotNull(unresolvedParam2.WaitForValueTcs);
Assert.False(unresolvedParam1.WaitForValueTcs.Task.IsCompleted);
Assert.False(unresolvedParam2.WaitForValueTcs.Task.IsCompleted);
}

private static ParameterProcessor CreateParameterProcessor(
Expand Down
Loading