Skip to content
Draft
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
162 changes: 162 additions & 0 deletions src/Stack.Tests/Commands/Helpers/StackActionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1017,4 +1017,166 @@ public async Task UpdateStack_WhenCheckingPullRequests_AndGitHubClientIsNotAvail
await stackActions.Invoking(async a => await a.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None, true))
.Should().ThrowAsync<InvalidOperationException>();
}

[Fact]
public async Task UpdateStack_UsingReplay_WhenConflictDetected_ThrowsConflictException()
{
// Arrange
var sourceBranch = Some.BranchName();
var feature = Some.BranchName();

var logger = XUnitLogger.CreateLogger<StackActions>(testOutputHelper);
var displayProvider = new TestDisplayProvider(testOutputHelper);
var gitClient = Substitute.For<IGitClient>();
var gitHubClient = Substitute.For<IGitHubClient>();
var conflictResolutionDetector = Substitute.For<IConflictResolutionDetector>();
var stack = new Model.Stack("Stack1", sourceBranch, new List<Model.Branch> { new(feature, []) });

var sourceTip = Some.Sha();
gitClient.GetBranchStatuses(Arg.Any<string[]>()).Returns(new Dictionary<string, GitBranchStatus>
{
{ sourceBranch, new GitBranchStatus(sourceBranch, $"origin/{sourceBranch}", true, false, 0, 0, new Commit(sourceTip, Some.Name())) },
{ feature, new GitBranchStatus(feature, $"origin/{feature}", true, false, 0, 0, new Commit(Some.Sha(), Some.Name())) }
});

gitClient.When(g => g.ReplayFromSourceBranch(feature, sourceBranch, sourceTip)).Throws(new ConflictException());

var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" };
var factory = Substitute.For<IGitClientFactory>();
factory.Create(Arg.Any<string>()).Returns(gitClient);
var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector);

// Act
var act = async () => await actions.UpdateStack(stack, UpdateStrategy.Replay, CancellationToken.None);

// Assert
await act.Should().ThrowAsync<Exception>().WithMessage("*Conflicts detected*");
}

[Fact]
public async Task UpdateStack_UsingReplay_WhenNoConflicts_CallsReplayForEachBranch()
{
// Arrange
var sourceBranch = Some.BranchName();
var feature1 = Some.BranchName();
var feature2 = Some.BranchName();

var logger = XUnitLogger.CreateLogger<StackActions>(testOutputHelper);
var displayProvider = new TestDisplayProvider(testOutputHelper);
var gitClient = Substitute.For<IGitClient>();
var gitHubClient = Substitute.For<IGitHubClient>();
var conflictResolutionDetector = Substitute.For<IConflictResolutionDetector>();
var stack = new Model.Stack("Stack1", sourceBranch, new List<Model.Branch>
{
new(feature1, [new(feature2, [])])
});

var sourceTip = Some.Sha();
var feature1Tip = Some.Sha();
gitClient.GetBranchStatuses(Arg.Any<string[]>()).Returns(new Dictionary<string, GitBranchStatus>
{
{ sourceBranch, new GitBranchStatus(sourceBranch, $"origin/{sourceBranch}", true, false, 0, 0, new Commit(sourceTip, Some.Name())) },
{ feature1, new GitBranchStatus(feature1, $"origin/{feature1}", true, false, 0, 0, new Commit(feature1Tip, Some.Name())) },
{ feature2, new GitBranchStatus(feature2, $"origin/{feature2}", true, false, 0, 0, new Commit(Some.Sha(), Some.Name())) }
});

var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" };
var factory = Substitute.For<IGitClientFactory>();
factory.Create(Arg.Any<string>()).Returns(gitClient);
var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector);

// Act
await actions.UpdateStack(stack, UpdateStrategy.Replay, CancellationToken.None);

// Assert
gitClient.Received(1).ReplayFromSourceBranch(feature1, sourceBranch, sourceTip);
gitClient.Received(1).ReplayFromSourceBranch(feature2, feature1, feature1Tip);
gitClient.DidNotReceive().ChangeBranch(Arg.Any<string>());
}

[Fact]
public async Task UpdateStack_UsingReplay_WhenBranchHasMergedPullRequest_SkipsBranch()
{
// Arrange
var sourceBranch = Some.BranchName();
var inactiveBranch = Some.BranchName();

var logger = XUnitLogger.CreateLogger<StackActions>(testOutputHelper);
var displayProvider = new TestDisplayProvider(testOutputHelper);
var gitClient = Substitute.For<IGitClient>();
var gitHubClient = new TestGitHubRepositoryBuilder()
.WithPullRequest(inactiveBranch, pr => pr.Merged())
.Build();
var conflictResolutionDetector = Substitute.For<IConflictResolutionDetector>();

var branchStatuses = new Dictionary<string, GitBranchStatus>
{
{ sourceBranch, new GitBranchStatus(sourceBranch, $"origin/{sourceBranch}", true, true, 0, 0, new Commit(Some.Sha(), Some.Name())) },
{ inactiveBranch, new GitBranchStatus(inactiveBranch, $"origin/{inactiveBranch}", true, false, 0, 0, new Commit(Some.Sha(), Some.Name())) }
};

gitClient.GetBranchStatuses(Arg.Any<string[]>()).Returns(branchStatuses);

var stack = new Model.Stack(
"Stack1",
sourceBranch,
new List<Model.Branch> { new(inactiveBranch, []) });

var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" };
var factory = Substitute.For<IGitClientFactory>();
factory.Create(executionContext.WorkingDirectory).Returns(gitClient);
factory.Create(Arg.Any<string>()).Returns(gitClient);

var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector);

// Act
await actions.UpdateStack(stack, UpdateStrategy.Replay, CancellationToken.None, true);

// Assert
gitClient.DidNotReceive().ChangeBranch(inactiveBranch);
gitClient.DidNotReceive().ReplayFromSourceBranch(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
gitClient.DidNotReceive().ReplayOntoNewParent(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
}

[Fact]
public async Task UpdateStack_UsingReplay_WhenBranchHasNoRemoteTrackingBranch_IsUpdated()
{
// Arrange
var sourceBranch = Some.BranchName();
var localOnlyBranch = Some.BranchName();

var logger = XUnitLogger.CreateLogger<StackActions>(testOutputHelper);
var displayProvider = new TestDisplayProvider(testOutputHelper);
var gitClient = Substitute.For<IGitClient>();
var gitHubClient = Substitute.For<IGitHubClient>();
var conflictResolutionDetector = Substitute.For<IConflictResolutionDetector>();

var sourceTip = Some.Sha();
var branchStatuses = new Dictionary<string, GitBranchStatus>
{
{ sourceBranch, new GitBranchStatus(sourceBranch, $"origin/{sourceBranch}", true, true, 0, 0, new Commit(sourceTip, Some.Name())) },
{ localOnlyBranch, new GitBranchStatus(localOnlyBranch, null, false, false, 0, 0, new Commit(Some.Sha(), Some.Name())) }
};

gitClient.GetBranchStatuses(Arg.Any<string[]>()).Returns(branchStatuses);

var stack = new Model.Stack(
"Stack1",
sourceBranch,
new List<Model.Branch> { new(localOnlyBranch, []) });

var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" };
var factory = Substitute.For<IGitClientFactory>();
factory.Create(executionContext.WorkingDirectory).Returns(gitClient);
factory.Create(Arg.Any<string>()).Returns(gitClient);

var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector);

// Act
await actions.UpdateStack(stack, UpdateStrategy.Replay, CancellationToken.None);

// Assert
gitClient.DidNotReceive().ChangeBranch(Arg.Any<string>());
gitClient.Received(1).ReplayFromSourceBranch(localOnlyBranch, sourceBranch, sourceTip);
}
}
34 changes: 17 additions & 17 deletions src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public async Task WhenNameIsProvided_DoesNotAskForName_SyncsCorrectStack()
inputProvider.Select(Questions.SelectUpdateStrategy, Arg.Any<UpdateStrategy[]>(), Arg.Any<CancellationToken>(), Arg.Any<Func<UpdateStrategy, string>>()).Returns(Task.FromResult(UpdateStrategy.Merge));

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

// Assert
stackActions.Received().PullChanges(Arg.Is<Model.Stack>(s => s.Name == "Stack1"));
Expand Down Expand Up @@ -98,7 +98,7 @@ public async Task WhenNameIsProvided_ButStackDoesNotExist_Throws()

// Act and assert
var invalidStackName = Some.Name();
await handler.Invoking(async h => await h.Handle(new SyncStackCommandInputs(invalidStackName, 5, false, false, false, false, false), CancellationToken.None))
await handler.Invoking(async h => await h.Handle(new SyncStackCommandInputs(invalidStackName, 5, false, false, null, false, false, false), CancellationToken.None))
.Should().ThrowAsync<InvalidOperationException>()
.WithMessage($"Stack '{invalidStackName}' not found.");
}
Expand Down Expand Up @@ -149,7 +149,7 @@ public async Task WhenOnASpecificBranchInTheStack_TheSameBranchIsSetAsCurrentAft
inputProvider.Select(Questions.SelectUpdateStrategy, Arg.Any<UpdateStrategy[]>(), Arg.Any<CancellationToken>(), Arg.Any<Func<UpdateStrategy, string>>()).Returns(Task.FromResult(UpdateStrategy.Merge));

// Act
await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false, false, false), CancellationToken.None);
await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, null, false, false, false), CancellationToken.None);

// Assert
stackActions.Received().PullChanges(Arg.Is<Model.Stack>(s => s.Name == "Stack1"));
Expand Down Expand Up @@ -197,7 +197,7 @@ public async Task WhenOnlyASingleStackExists_DoesNotAskForStackName_SyncsStack()
inputProvider.Select(Questions.SelectUpdateStrategy, Arg.Any<UpdateStrategy[]>(), Arg.Any<CancellationToken>(), Arg.Any<Func<UpdateStrategy, string>>()).Returns(Task.FromResult(UpdateStrategy.Merge));

// Act
await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false, false, false), CancellationToken.None);
await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, null, false, false, false), CancellationToken.None);

// Assert
stackActions.Received().PullChanges(Arg.Is<Model.Stack>(s => s.Name == "Stack1"));
Expand Down Expand Up @@ -248,7 +248,7 @@ public async Task WhenRebaseIsProvided_SyncsStackUsingRebase()
inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any<CancellationToken>(), Arg.Any<bool>()).Returns(Task.FromResult(true));

// Act
await handler.Handle(new SyncStackCommandInputs(null, 5, true, false, false, false, false), CancellationToken.None);
await handler.Handle(new SyncStackCommandInputs(null, 5, true, false, null, false, false, false), CancellationToken.None);

// Assert
stackActions.Received().PullChanges(Arg.Is<Model.Stack>(s => s.Name == "Stack1"));
Expand Down Expand Up @@ -298,7 +298,7 @@ public async Task WhenMergeIsProvided_SyncsStackUsingMerge()
inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any<CancellationToken>()).Returns(true);

// Act
await handler.Handle(new SyncStackCommandInputs(null, 5, false, true, false, false, false), CancellationToken.None);
await handler.Handle(new SyncStackCommandInputs(null, 5, false, true, null, false, false, false), CancellationToken.None);

// Assert
stackActions.Received().PullChanges(Arg.Is<Model.Stack>(s => s.Name == "Stack1"));
Expand Down Expand Up @@ -348,7 +348,7 @@ public async Task WhenNotSpecifyingRebaseOrMerge_AndUpdateSettingIsRebase_SyncsS
inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any<CancellationToken>()).Returns(true);

// Act
await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false, false), CancellationToken.None);
await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, null, false, false, false), CancellationToken.None);

// Assert
stackActions.Received().PullChanges(Arg.Is<Model.Stack>(s => s.Name == "Stack1"));
Expand Down Expand Up @@ -398,7 +398,7 @@ public async Task WhenNotSpecifyingRebaseOrMerge_AndUpdateSettingIsMerge_SyncsSt
inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any<CancellationToken>()).Returns(true);

// Act
await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false, false), CancellationToken.None);
await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, null, false, false, false), CancellationToken.None);

// Assert
stackActions.Received().PullChanges(Arg.Is<Model.Stack>(s => s.Name == "Stack1"));
Expand Down Expand Up @@ -448,7 +448,7 @@ public async Task WhenGitConfigValueIsSetToMerge_ButRebaseIsSpecified_SyncsStack
inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any<CancellationToken>()).Returns(true);

// Act
await handler.Handle(new SyncStackCommandInputs(null, 5, true, null, false, false, false), CancellationToken.None);
await handler.Handle(new SyncStackCommandInputs(null, 5, true, null, null, false, false, false), CancellationToken.None);

// Assert
stackActions.Received().PullChanges(Arg.Is<Model.Stack>(s => s.Name == "Stack1"));
Expand Down Expand Up @@ -498,7 +498,7 @@ public async Task WhenGitConfigValueIsSetToRebase_ButMergeIsSpecified_SyncsStack
inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any<CancellationToken>()).Returns(true);

// Act
await handler.Handle(new SyncStackCommandInputs(null, 5, null, true, false, false, false), CancellationToken.None);
await handler.Handle(new SyncStackCommandInputs(null, 5, null, true, null, false, false, false), CancellationToken.None);

// Assert
stackActions.Received().PullChanges(Arg.Is<Model.Stack>(s => s.Name == "Stack1"));
Expand Down Expand Up @@ -549,7 +549,7 @@ public async Task WhenNotSpecifyingRebaseOrMerge_AndNoUpdateSettingExists_AndMer
inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any<CancellationToken>()).Returns(true);

// Act
await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false, false), CancellationToken.None);
await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, null, false, false, false), CancellationToken.None);

// Assert
stackActions.Received().PullChanges(Arg.Is<Model.Stack>(s => s.Name == "Stack1"));
Expand Down Expand Up @@ -600,7 +600,7 @@ public async Task WhenNotSpecifyingRebaseOrMerge_AndNoUpdateSettingsExists_AndRe
inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any<CancellationToken>()).Returns(true);

// Act
await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false, false), CancellationToken.None);
await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, null, false, false, false), CancellationToken.None);

// Assert
stackActions.Received().PullChanges(Arg.Is<Model.Stack>(s => s.Name == "Stack1"));
Expand Down Expand Up @@ -629,7 +629,7 @@ public async Task WhenBothRebaseAndMergeAreSpecified_AnErrorIsThrown()

// Act and assert
await handler
.Invoking(h => h.Handle(new SyncStackCommandInputs(null, 5, true, true, false, false, false), CancellationToken.None))
.Invoking(h => h.Handle(new SyncStackCommandInputs(null, 5, true, true, null, false, false, false), CancellationToken.None))
.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("Cannot specify both rebase and merge.");
}
Expand Down Expand Up @@ -675,7 +675,7 @@ public async Task WhenConfirmOptionIsProvided_DoesNotAskForConfirmation()
inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>(), Arg.Any<CancellationToken>()).Returns("Stack1");

// Act
await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, true, false, false), CancellationToken.None);
await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, null, true, false, false), CancellationToken.None);

// Assert
stackActions.Received().PullChanges(Arg.Is<Model.Stack>(s => s.Name == "Stack1"));
Expand Down Expand Up @@ -727,7 +727,7 @@ public async Task WhenNoPushOptionIsProvided_DoesNotPushChangesToRemote()
inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any<CancellationToken>()).Returns(true);

// Act
await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false, true, false), CancellationToken.None);
await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, null, false, true, false), CancellationToken.None);

// Assert
stackActions.Received().PullChanges(Arg.Is<Model.Stack>(s => s.Name == "Stack1"));
Expand Down Expand Up @@ -775,7 +775,7 @@ public async Task WhenCheckPullRequestsIsTrue_UpdatesStackWithCheckPullRequestsE
inputProvider.Select(Questions.SelectUpdateStrategy, Arg.Any<UpdateStrategy[]>(), Arg.Any<CancellationToken>(), Arg.Any<Func<UpdateStrategy, string>>()).Returns(Task.FromResult(UpdateStrategy.Merge));

// Act
await handler.Handle(new SyncStackCommandInputs("Stack1", 5, false, false, false, false, true), CancellationToken.None);
await handler.Handle(new SyncStackCommandInputs("Stack1", 5, false, false, null, false, false, true), CancellationToken.None);

// Assert
stackActions.Received().PullChanges(Arg.Is<Model.Stack>(s => s.Name == "Stack1"));
Expand Down Expand Up @@ -823,7 +823,7 @@ public async Task WhenCheckPullRequestsIsFalse_UpdatesStackWithCheckPullRequests
inputProvider.Select(Questions.SelectUpdateStrategy, Arg.Any<UpdateStrategy[]>(), Arg.Any<CancellationToken>(), Arg.Any<Func<UpdateStrategy, string>>()).Returns(Task.FromResult(UpdateStrategy.Merge));

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

// Assert
stackActions.Received().PullChanges(Arg.Is<Model.Stack>(s => s.Name == "Stack1"));
Expand Down
Loading
Loading