Skip to content
Closed
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
111 changes: 111 additions & 0 deletions GitHubExtension.Test/Controls/MutationCommandsFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using GitHubExtension.Controls;
using GitHubExtension.Controls.Commands;
using GitHubExtension.DataManager;
using GitHubExtension.Helpers;
using Moq;

namespace GitHubExtension.Test.Controls;

[TestClass]
public class MutationCommandsFactoryTests
{
private static (MutationCommandsFactory Factory, Mock<IGitHubMutationManager> Manager, MutationMediator Mediator) CreateFactory()
{
var manager = new Mock<IGitHubMutationManager>();
var mediator = new MutationMediator();
var resources = new Mock<IResources>();
resources.Setup(x => x.GetResource(It.IsAny<string>(), null)).Returns<string, object>((key, _) => key);
var factory = new MutationCommandsFactory(manager.Object, mediator, resources.Object);
return (factory, manager, mediator);
}

private static Mock<IIssue> CreateIssue(string state)
{
var issue = new Mock<IIssue>();
issue.Setup(x => x.State).Returns(state);
issue.Setup(x => x.HtmlUrl).Returns("https://github.com/owner/repo/issues/1");
issue.Setup(x => x.Number).Returns(1);
issue.Setup(x => x.Title).Returns("Title");
return issue;
}

private static Mock<IPullRequest> CreatePullRequest(string state)
{
var pr = new Mock<IPullRequest>();
pr.Setup(x => x.State).Returns(state);
pr.Setup(x => x.HtmlUrl).Returns("https://github.com/owner/repo/pull/2");
pr.Setup(x => x.Number).Returns(2);
pr.Setup(x => x.Title).Returns("Title");
pr.Setup(x => x.SourceBranch).Returns("feature");
return pr;
}

[TestMethod]
[TestCategory("Unit")]
public void GetIssueCommands_OpenIssue_ReturnsCloseCommand()
{
var (factory, _, _) = CreateFactory();
var commands = factory.GetIssueCommands(CreateIssue("Open").Object).ToList();

Assert.AreEqual(1, commands.Count);
Assert.AreEqual("Commands_CloseIssue", commands[0].Command!.Name);
Assert.IsTrue(commands[0].IsCritical);
}

[TestMethod]
[TestCategory("Unit")]
public void GetIssueCommands_ClosedIssue_ReturnsReopenCommand()
{
var (factory, _, _) = CreateFactory();
var commands = factory.GetIssueCommands(CreateIssue("Closed").Object).ToList();

Assert.AreEqual(1, commands.Count);
Assert.AreEqual("Commands_ReopenIssue", commands[0].Command!.Name);
Assert.IsFalse(commands[0].IsCritical);
}

[TestMethod]
[TestCategory("Unit")]
public void GetPullRequestCommands_OpenPullRequest_ReturnsMergeAndCloseCommands()
{
var (factory, _, _) = CreateFactory();
var commands = factory.GetPullRequestCommands(CreatePullRequest("Open").Object).ToList();

Assert.AreEqual(2, commands.Count);
Assert.AreEqual("Commands_MergePullRequest", commands[0].Command!.Name);
Assert.AreEqual("Commands_ClosePullRequest", commands[1].Command!.Name);
Assert.IsTrue(commands[0].IsCritical);
Assert.IsTrue(commands[1].IsCritical);
}

[TestMethod]
[TestCategory("Unit")]
public void GetPullRequestCommands_ClosedPullRequest_ReturnsReopenCommand()
{
var (factory, _, _) = CreateFactory();
var commands = factory.GetPullRequestCommands(CreatePullRequest("Closed").Object).ToList();

Assert.AreEqual(1, commands.Count);
Assert.AreEqual("Commands_ReopenPullRequest", commands[0].Command!.Name);
Assert.IsFalse(commands[0].IsCritical);
}

[TestMethod]
[TestCategory("Unit")]
public void MergeCommand_Invoke_DefersToConfirmationWithoutCallingManager()
{
var (factory, manager, _) = CreateFactory();
var pr = CreatePullRequest("Open");

var commands = factory.GetPullRequestCommands(pr.Object).ToList();
var merge = (Microsoft.CommandPalette.Extensions.Toolkit.InvokableCommand)commands[0].Command!;
var result = merge.Invoke();

Assert.IsNotNull(result);
manager.Verify(x => x.MergePullRequestAsync(It.IsAny<IPullRequest>()), Times.Never);
}
}
24 changes: 19 additions & 5 deletions GitHubExtension.Test/Controls/SearchPagesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

using System.Net;
using GitHubExtension.Controls;
using GitHubExtension.Controls.Commands;
using GitHubExtension.Controls.Pages;
using GitHubExtension.DataManager;
using GitHubExtension.DataModel.Enums;
using GitHubExtension.Helpers;
using Moq;
Expand All @@ -15,6 +17,14 @@ namespace GitHubExtension.Test.Controls;
[TestClass]
public class SearchPagesTests
{
private static (MutationCommandsFactory Factory, MutationMediator Mediator) CreateMutationDeps(Mock<IResources> resources)
{
var mutationManager = new Mock<IGitHubMutationManager>();
var mediator = new MutationMediator();
var factory = new MutationCommandsFactory(mutationManager.Object, mediator, resources.Object);
return (factory, mediator);
}

private (Mock<ICacheDataManager> CacheDataManager, Mock<IResources> Resources, Mock<ISearch> Search) CreateCommonMocks(SearchType type, string searchString = "test search string")
{
var cacheDataManager = new Mock<ICacheDataManager>();
Expand All @@ -33,11 +43,12 @@ public class SearchPagesTests
public void SearchPagesCreate_CreatesPagesForBothTypes()
{
var (cacheDataManager, resources, search) = CreateCommonMocks(SearchType.Issues, "test search string is:pr");
var issuesSearchPage = new IssuesSearchPage(search.Object, cacheDataManager.Object, resources.Object);
var (mutationFactory, mutationMediator) = CreateMutationDeps(resources);
var issuesSearchPage = new IssuesSearchPage(search.Object, cacheDataManager.Object, resources.Object, mutationFactory, mutationMediator);
Assert.IsNotNull(issuesSearchPage);

search.Setup(x => x.Type).Returns(SearchType.PullRequests);
var pullRequestsSearchPage = new PullRequestsSearchPage(search.Object, cacheDataManager.Object, resources.Object);
var pullRequestsSearchPage = new PullRequestsSearchPage(search.Object, cacheDataManager.Object, resources.Object, mutationFactory, mutationMediator);
Assert.IsNotNull(pullRequestsSearchPage);

search.Setup(x => x.Type).Returns(SearchType.Repositories);
Expand Down Expand Up @@ -82,7 +93,8 @@ public void GetItemsFromSearchPage_ReturnsExpectedItems(SearchType type)

if (type == SearchType.PullRequests)
{
var page = new PullRequestsSearchPage(search.Object, cacheDataManager.Object, resources.Object);
var (mutationFactory, mutationMediator) = CreateMutationDeps(resources);
var page = new PullRequestsSearchPage(search.Object, cacheDataManager.Object, resources.Object, mutationFactory, mutationMediator);
var pull1 = new Mock<IPullRequest>();
var pull2 = new Mock<IPullRequest>();
pull1.Setup(x => x.Title).Returns("Title1");
Expand All @@ -103,7 +115,8 @@ public void GetItemsFromSearchPage_ReturnsExpectedItems(SearchType type)
}
else
{
var page = new IssuesSearchPage(search.Object, cacheDataManager.Object, resources.Object);
var (mutationFactory, mutationMediator) = CreateMutationDeps(resources);
var page = new IssuesSearchPage(search.Object, cacheDataManager.Object, resources.Object, mutationFactory, mutationMediator);
var issue1 = new Mock<IIssue>();
var issue2 = new Mock<IIssue>();
issue1.Setup(x => x.Title).Returns("Title1");
Expand All @@ -129,7 +142,8 @@ public void SearchPageGetItems_RateLimitExceededExceptionIsCaught()
var (cacheDataManager, resources, search) = CreateCommonMocks(SearchType.PullRequests, "test search string is:pr");
resources.Setup(x => x.GetResource("Pages_Error_Title", null)).Returns("Error fetching items");

var pullRequestsSearchPage = new PullRequestsSearchPage(search.Object, cacheDataManager.Object, resources.Object);
var (mutationFactory, mutationMediator) = CreateMutationDeps(resources);
var pullRequestsSearchPage = new PullRequestsSearchPage(search.Object, cacheDataManager.Object, resources.Object, mutationFactory, mutationMediator);

var mockResponse = new Mock<IResponse>();
mockResponse.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.Forbidden);
Expand Down
7 changes: 6 additions & 1 deletion GitHubExtension.Test/Controls/TopLevelSearchesTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
// See the LICENSE file in the project root for more information.

using GitHubExtension.Controls;
using GitHubExtension.Controls.Commands;
using GitHubExtension.Controls.Forms;
using GitHubExtension.Controls.Pages;
using GitHubExtension.DataManager;
using GitHubExtension.DataModel;
using GitHubExtension.Helpers;
using GitHubExtension.PersistentData;
Expand Down Expand Up @@ -110,7 +112,10 @@ public async Task Integration_AddNewTopLevelCommand(string searchString, string
{
var mockDeveloperIdProvider = TestHelpers.CreateMockDeveloperIdProvider();
var mockCacheDataManager = new Mock<ICacheDataManager>().Object;
var searchPageFactory = new SearchPageFactory(mockCacheDataManager, persistentDataManager, resources, mediator);
var mutationManager = new Mock<IGitHubMutationManager>().Object;
var mutationMediator = new MutationMediator();
var mutationCommandsFactory = new MutationCommandsFactory(mutationManager, mutationMediator, resources);
var searchPageFactory = new SearchPageFactory(mockCacheDataManager, persistentDataManager, resources, mediator, mutationCommandsFactory, mutationMediator);

var addSearchForm = new SaveSearchForm(persistentDataManager, resources, mediator);

Expand Down
37 changes: 37 additions & 0 deletions GitHubExtension/Controls/Commands/ConfirmedCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.CommandPalette.Extensions.Toolkit;

namespace GitHubExtension.Controls.Commands;

// Wraps a destructive command in a confirmation prompt. When the user confirms,
// the inner command runs. Used for irreversible actions such as merging or
// closing.
internal sealed partial class ConfirmedCommand : InvokableCommand
{
private readonly InvokableCommand _innerCommand;
private readonly string _confirmTitle;
private readonly string _confirmDescription;

internal ConfirmedCommand(InvokableCommand innerCommand, string name, IconInfo icon, string confirmTitle, string confirmDescription)
{
_innerCommand = innerCommand;
_confirmTitle = confirmTitle;
_confirmDescription = confirmDescription;
Name = name;
Icon = icon;
}

public override CommandResult Invoke()
{
return CommandResult.Confirm(new ConfirmationArgs
{
Title = _confirmTitle,
Description = _confirmDescription,
PrimaryCommand = _innerCommand,
IsPrimaryCommandCritical = true,
});
}
}
50 changes: 50 additions & 0 deletions GitHubExtension/Controls/Commands/GitHubMutationCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using GitHubExtension.Helpers;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Serilog;

namespace GitHubExtension.Controls.Commands;

// Runs a single write action against GitHub, showing a success or error toast
// and notifying the mutation mediator so open surfaces can refresh. Non
// destructive actions use this command directly; destructive actions wrap it
// in a ConfirmedCommand.
internal sealed partial class GitHubMutationCommand : InvokableCommand
{
private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(GitHubMutationCommand));

private readonly Func<Task> _mutation;
private readonly string _successMessage;
private readonly string _errorMessage;
private readonly MutationMediator _mediator;

internal GitHubMutationCommand(string name, IconInfo icon, Func<Task> mutation, string successMessage, string errorMessage, MutationMediator mediator)
{
Name = name;
Icon = icon;
_mutation = mutation;
_successMessage = successMessage;
_errorMessage = errorMessage;
_mediator = mediator;
}

public override CommandResult Invoke()
{
try
{
_mutation().GetAwaiter().GetResult();
ToastHelper.ShowSuccessToast(_successMessage);
_mediator.NotifyMutationCompleted();
}
catch (Exception ex)
{
_log.Error(ex, "GitHub write action failed.");
ToastHelper.ShowErrorToast(_errorMessage);
}

return CommandResult.KeepOpen();
}
}
Loading
Loading