Skip to content
Merged
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,22 @@ Options:
-?, -h, --help Show help and usage information
```

#### `stack rename` <!-- omit from toc -->

Rename a stack.

```shell
Usage:
stack rename [options]

Options:
--working-dir The path to the directory containing the git repository. Defaults to the current directory.
--verbose Show verbose output.
-s, --stack The name of the stack.
-n, --name The new name for the stack.
-?, -h, --help Show help and usage information
```

### Branch commands <!-- omit from toc -->

#### `stack update` <!-- omit from toc -->
Expand Down
320 changes: 320 additions & 0 deletions src/Stack.Tests/Commands/Stack/RenameStackCommandHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
using FluentAssertions;
using NSubstitute;
using Meziantou.Extensions.Logging.Xunit;
using Microsoft.Extensions.Logging;
using Stack.Commands;
using Stack.Commands.Helpers;
using Stack.Config;
using Stack.Git;
using Stack.Infrastructure;
using Stack.Tests.Helpers;
using Xunit.Abstractions;

namespace Stack.Tests.Commands.Stack;

public class RenameStackCommandHandlerTests(ITestOutputHelper testOutputHelper)
{
[Fact]
public async Task WhenNoInputsAreProvided_AsksForStackAndNewName_RenamesStack()
{
// Arrange
var sourceBranch = Some.BranchName();
var remoteUri = Some.HttpsUri().ToString();

var stackConfig = new TestStackConfigBuilder()
.WithStack(stack => stack
.WithName("Stack1")
.WithRemoteUri(remoteUri)
.WithSourceBranch(sourceBranch))
.WithStack(stack => stack
.WithName("Stack2")
.WithRemoteUri(remoteUri)
.WithSourceBranch(sourceBranch))
.Build();

var inputProvider = Substitute.For<IInputProvider>();
var logger = XUnitLogger.CreateLogger<RenameStackCommandHandler>(testOutputHelper);
var gitClient = Substitute.For<IGitClient>();

gitClient.GetRemoteUri().Returns(remoteUri);
gitClient.GetCurrentBranch().Returns(sourceBranch);

var handler = new RenameStackCommandHandler(inputProvider, logger, gitClient, stackConfig);

inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>(), Arg.Any<CancellationToken>()).Returns("Stack1");
inputProvider.Text(Questions.StackName, Arg.Any<CancellationToken>()).Returns("RenamedStack");

// Act
await handler.Handle(RenameStackCommandInputs.Empty, CancellationToken.None);

// Assert
stackConfig.Stacks.Should().HaveCount(2);
stackConfig.Stacks.Should().Contain(s => s.Name == "RenamedStack");
stackConfig.Stacks.Should().Contain(s => s.Name == "Stack2");
stackConfig.Stacks.Should().NotContain(s => s.Name == "Stack1");
}

[Fact]
public async Task WhenStackNameProvided_AsksOnlyForNewName_RenamesStack()
{
// Arrange
var sourceBranch = Some.BranchName();
var remoteUri = Some.HttpsUri().ToString();

var stackConfig = new TestStackConfigBuilder()
.WithStack(stack => stack
.WithName("Stack1")
.WithRemoteUri(remoteUri)
.WithSourceBranch(sourceBranch))
.Build();

var inputProvider = Substitute.For<IInputProvider>();
var logger = XUnitLogger.CreateLogger<RenameStackCommandHandler>(testOutputHelper);
var gitClient = Substitute.For<IGitClient>();

gitClient.GetRemoteUri().Returns(remoteUri);
gitClient.GetCurrentBranch().Returns(sourceBranch);

var handler = new RenameStackCommandHandler(inputProvider, logger, gitClient, stackConfig);

inputProvider.Text(Questions.StackName, Arg.Any<CancellationToken>()).Returns("RenamedStack");

// Act
await handler.Handle(new RenameStackCommandInputs("Stack1", null), CancellationToken.None);

// Assert
await inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any<string[]>(), Arg.Any<CancellationToken>());
stackConfig.Stacks.Should().HaveCount(1);
stackConfig.Stacks.Should().Contain(s => s.Name == "RenamedStack");
}

[Fact]
public async Task WhenNewNameProvided_AsksOnlyForStack_RenamesStack()
{
// Arrange
var sourceBranch = Some.BranchName();
var remoteUri = Some.HttpsUri().ToString();

var stackConfig = new TestStackConfigBuilder()
.WithStack(stack => stack
.WithName("Stack1")
.WithRemoteUri(remoteUri)
.WithSourceBranch(sourceBranch))
.Build();

var inputProvider = Substitute.For<IInputProvider>();
var logger = XUnitLogger.CreateLogger<RenameStackCommandHandler>(testOutputHelper);
var gitClient = Substitute.For<IGitClient>();

gitClient.GetRemoteUri().Returns(remoteUri);
gitClient.GetCurrentBranch().Returns(sourceBranch);

var handler = new RenameStackCommandHandler(inputProvider, logger, gitClient, stackConfig);

inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>(), Arg.Any<CancellationToken>()).Returns("Stack1");

// Act
await handler.Handle(new RenameStackCommandInputs(null, "RenamedStack"), CancellationToken.None);

// Assert
await inputProvider.DidNotReceive().Text(Questions.StackName, Arg.Any<CancellationToken>());
stackConfig.Stacks.Should().HaveCount(1);
stackConfig.Stacks.Should().Contain(s => s.Name == "RenamedStack");
}

[Fact]
public async Task WhenBothInputsProvided_DoesNotAskForAnything_RenamesStack()
{
// Arrange
var sourceBranch = Some.BranchName();
var remoteUri = Some.HttpsUri().ToString();

var stackConfig = new TestStackConfigBuilder()
.WithStack(stack => stack
.WithName("Stack1")
.WithRemoteUri(remoteUri)
.WithSourceBranch(sourceBranch))
.Build();

var inputProvider = Substitute.For<IInputProvider>();
var logger = XUnitLogger.CreateLogger<RenameStackCommandHandler>(testOutputHelper);
var gitClient = Substitute.For<IGitClient>();

gitClient.GetRemoteUri().Returns(remoteUri);
gitClient.GetCurrentBranch().Returns(sourceBranch);

var handler = new RenameStackCommandHandler(inputProvider, logger, gitClient, stackConfig);

// Act
await handler.Handle(new RenameStackCommandInputs("Stack1", "RenamedStack"), CancellationToken.None);

// Assert
await inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any<string[]>(), Arg.Any<CancellationToken>());
await inputProvider.DidNotReceive().Text(Questions.StackName, Arg.Any<CancellationToken>());
stackConfig.Stacks.Should().HaveCount(1);
stackConfig.Stacks.Should().Contain(s => s.Name == "RenamedStack");
}

[Fact]
public async Task WhenStackDoesNotExist_ThrowsException()
{
// Arrange
var sourceBranch = Some.BranchName();
var remoteUri = Some.HttpsUri().ToString();

var stackConfig = new TestStackConfigBuilder()
.WithStack(stack => stack
.WithName("Stack1")
.WithRemoteUri(remoteUri)
.WithSourceBranch(sourceBranch))
.Build();

var inputProvider = Substitute.For<IInputProvider>();
var logger = XUnitLogger.CreateLogger<RenameStackCommandHandler>(testOutputHelper);
var gitClient = Substitute.For<IGitClient>();

gitClient.GetRemoteUri().Returns(remoteUri);
gitClient.GetCurrentBranch().Returns(sourceBranch);

var handler = new RenameStackCommandHandler(inputProvider, logger, gitClient, stackConfig);

// Act & Assert
var act = async () => await handler.Handle(new RenameStackCommandInputs("NonExistentStack", "NewName"), CancellationToken.None);
await act.Should().ThrowAsync<InvalidOperationException>().WithMessage("Stack not found.");
}

[Fact]
public async Task WhenNewNameAlreadyExists_ThrowsException()
{
// Arrange
var sourceBranch = Some.BranchName();
var remoteUri = Some.HttpsUri().ToString();

var stackConfig = new TestStackConfigBuilder()
.WithStack(stack => stack
.WithName("Stack1")
.WithRemoteUri(remoteUri)
.WithSourceBranch(sourceBranch))
.WithStack(stack => stack
.WithName("Stack2")
.WithRemoteUri(remoteUri)
.WithSourceBranch(sourceBranch))
.Build();

var inputProvider = Substitute.For<IInputProvider>();
var logger = XUnitLogger.CreateLogger<RenameStackCommandHandler>(testOutputHelper);
var gitClient = Substitute.For<IGitClient>();

gitClient.GetRemoteUri().Returns(remoteUri);
gitClient.GetCurrentBranch().Returns(sourceBranch);

var handler = new RenameStackCommandHandler(inputProvider, logger, gitClient, stackConfig);

// Act & Assert
var act = async () => await handler.Handle(new RenameStackCommandInputs("Stack1", "Stack2"), CancellationToken.None);
await act.Should().ThrowAsync<InvalidOperationException>().WithMessage("A stack with the name 'Stack2' already exists for this remote.");
}

[Fact]
public async Task WhenRenamingToSameName_DoesNotThrowException()
{
// Arrange
var sourceBranch = Some.BranchName();
var remoteUri = Some.HttpsUri().ToString();

var stackConfig = new TestStackConfigBuilder()
.WithStack(stack => stack
.WithName("Stack1")
.WithRemoteUri(remoteUri)
.WithSourceBranch(sourceBranch))
.Build();

var inputProvider = Substitute.For<IInputProvider>();
var logger = XUnitLogger.CreateLogger<RenameStackCommandHandler>(testOutputHelper);
var gitClient = Substitute.For<IGitClient>();

gitClient.GetRemoteUri().Returns(remoteUri);
gitClient.GetCurrentBranch().Returns(sourceBranch);

var handler = new RenameStackCommandHandler(inputProvider, logger, gitClient, stackConfig);

// Act
await handler.Handle(new RenameStackCommandInputs("Stack1", "Stack1"), CancellationToken.None);

// Assert
stackConfig.Stacks.Should().HaveCount(1);
stackConfig.Stacks.Should().Contain(s => s.Name == "Stack1");
}

[Fact]
public async Task WhenStacksWithSameNameExistAcrossDifferentRemotes_AllowsRename()
{
// Arrange
var sourceBranch = Some.BranchName();
var remoteUri1 = Some.HttpsUri().ToString();
var remoteUri2 = Some.HttpsUri().ToString();

var stackConfig = new TestStackConfigBuilder()
.WithStack(stack => stack
.WithName("Stack1")
.WithRemoteUri(remoteUri1)
.WithSourceBranch(sourceBranch))
.WithStack(stack => stack
.WithName("ExistingName")
.WithRemoteUri(remoteUri2)
.WithSourceBranch(sourceBranch))
.Build();

var inputProvider = Substitute.For<IInputProvider>();
var logger = XUnitLogger.CreateLogger<RenameStackCommandHandler>(testOutputHelper);
var gitClient = Substitute.For<IGitClient>();

gitClient.GetRemoteUri().Returns(remoteUri1);
gitClient.GetCurrentBranch().Returns(sourceBranch);

var handler = new RenameStackCommandHandler(inputProvider, logger, gitClient, stackConfig);

// Act - Rename Stack1 to ExistingName (which exists on a different remote)
await handler.Handle(new RenameStackCommandInputs("Stack1", "ExistingName"), CancellationToken.None);

// Assert
stackConfig.Stacks.Should().HaveCount(2);
stackConfig.Stacks.Should().Contain(s => s.Name == "ExistingName" && s.RemoteUri == remoteUri1);
stackConfig.Stacks.Should().Contain(s => s.Name == "ExistingName" && s.RemoteUri == remoteUri2);
stackConfig.Stacks.Should().NotContain(s => s.Name == "Stack1");
}

[Fact]
public async Task WhenNoStacksExistForRemote_LogsNoStacksMessageAndReturns()
{
// Arrange
var sourceBranch = Some.BranchName();
var remoteUri1 = Some.HttpsUri().ToString();
var remoteUri2 = Some.HttpsUri().ToString();

// Create a stack config with stacks only for a different remote
var stackConfig = new TestStackConfigBuilder()
.WithStack(stack => stack
.WithName("Stack1")
.WithRemoteUri(remoteUri2)
.WithSourceBranch(sourceBranch))
.Build();

var inputProvider = Substitute.For<IInputProvider>();
var logger = XUnitLogger.CreateLogger<RenameStackCommandHandler>(testOutputHelper);
var gitClient = Substitute.For<IGitClient>();

gitClient.GetRemoteUri().Returns(remoteUri1); // Different remote than the stack
gitClient.GetCurrentBranch().Returns(sourceBranch);

var handler = new RenameStackCommandHandler(inputProvider, logger, gitClient, stackConfig);

// Act
await handler.Handle(new RenameStackCommandInputs("Stack1", "NewName"), CancellationToken.None);

// Assert
// Verify the stack config was not modified
stackConfig.Stacks.Should().HaveCount(1);
stackConfig.Stacks.Should().Contain(s => s.Name == "Stack1" && s.RemoteUri == remoteUri2);
}
}
Loading
Loading