From 1e5f6acdaf6baa6669d018c7164f29303c19112f Mon Sep 17 00:00:00 2001 From: Yan Sklyarenko Date: Sat, 6 Oct 2018 22:12:37 +0200 Subject: [PATCH 1/5] (GH-14) Add test cases for TFS calls --- src/Cake.Tfs.Tests/Cake.Tfs.Tests.csproj | 1 + .../ExceptionAssertExtensions.cs | 19 + .../Fakes/FakeAllSetGitClientFactory.cs | 106 +++++ .../PullRequest/Fakes/FakeGitClientFactory.cs | 24 ++ .../FakeNullForMethodsGitClientFactory.cs | 21 + .../Fakes/FakeNullGitClientFactory.cs | 27 ++ .../PullRequest/PullRequestFixture.cs | 41 ++ .../PullRequest/TfsPullRequestTests.cs | 391 +++++++++++++++++- .../PullRequest/GitClient/GitClientFactory.cs | 46 +++ .../GitClient/IGitClientFactory.cs | 30 ++ src/Cake.Tfs/PullRequest/TfsPullRequest.cs | 48 +-- src/Cake.Tfs/TfsAliases.PullRequest.cs | 6 +- 12 files changed, 708 insertions(+), 52 deletions(-) create mode 100644 src/Cake.Tfs.Tests/PullRequest/Fakes/FakeAllSetGitClientFactory.cs create mode 100644 src/Cake.Tfs.Tests/PullRequest/Fakes/FakeGitClientFactory.cs create mode 100644 src/Cake.Tfs.Tests/PullRequest/Fakes/FakeNullForMethodsGitClientFactory.cs create mode 100644 src/Cake.Tfs.Tests/PullRequest/Fakes/FakeNullGitClientFactory.cs create mode 100644 src/Cake.Tfs.Tests/PullRequest/PullRequestFixture.cs create mode 100644 src/Cake.Tfs/PullRequest/GitClient/GitClientFactory.cs create mode 100644 src/Cake.Tfs/PullRequest/GitClient/IGitClientFactory.cs diff --git a/src/Cake.Tfs.Tests/Cake.Tfs.Tests.csproj b/src/Cake.Tfs.Tests/Cake.Tfs.Tests.csproj index 9e7ae2ec..de78f132 100644 --- a/src/Cake.Tfs.Tests/Cake.Tfs.Tests.csproj +++ b/src/Cake.Tfs.Tests/Cake.Tfs.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Cake.Tfs.Tests/ExceptionAssertExtensions.cs b/src/Cake.Tfs.Tests/ExceptionAssertExtensions.cs index f90be701..0518f010 100644 --- a/src/Cake.Tfs.Tests/ExceptionAssertExtensions.cs +++ b/src/Cake.Tfs.Tests/ExceptionAssertExtensions.cs @@ -1,6 +1,7 @@ namespace Cake.Tfs { using System; + using Cake.Tfs.PullRequest; using Xunit; /// @@ -49,5 +50,23 @@ public static void IsInvalidOperationException(this Exception exception) { Assert.IsType(exception); } + + /// + /// Checks if an exception is of type . + /// + /// Exception to check. + public static void IsUrlFormatException(this Exception exception) + { + Assert.IsType(exception); + } + + /// + /// Checks if an exception is of type . + /// + /// Exception to check. + public static void IsTfsPullRequestNotFoundException(this Exception exception) + { + Assert.IsType(exception); + } } } diff --git a/src/Cake.Tfs.Tests/PullRequest/Fakes/FakeAllSetGitClientFactory.cs b/src/Cake.Tfs.Tests/PullRequest/Fakes/FakeAllSetGitClientFactory.cs new file mode 100644 index 00000000..64de027b --- /dev/null +++ b/src/Cake.Tfs.Tests/PullRequest/Fakes/FakeAllSetGitClientFactory.cs @@ -0,0 +1,106 @@ +namespace Cake.Tfs.Tests.PullRequest.Fakes +{ + using System; + using System.Collections.Generic; + using System.Threading; + using Cake.Tfs.Authentication; + using Cake.Tfs.PullRequest; + using Microsoft.TeamFoundation.SourceControl.WebApi; + using Moq; + + public class FakeAllSetGitClientFactory : FakeGitClientFactory + { + public override GitHttpClient CreateGitClient(Uri collectionUrl, ITfsCredentials credentials) + { + var mock = new Mock(MockBehavior.Loose, collectionUrl, credentials.ToVssCredentials()); + + mock.Setup(arg => arg.GetPullRequestAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, null, null, null, null, null, default(CancellationToken))) + .ReturnsAsync((string project1, string repoId1, int prId, int i1, int i2, int i3, bool b1, bool b2, object o1, CancellationToken c1) => new GitPullRequest + { + PullRequestId = prId, + Repository = new GitRepository + { + Id = Guid.NewGuid(), + Name = repoId1 + }, + SourceRefName = "foo", + TargetRefName = "master", + CodeReviewId = 123, + LastMergeSourceCommit = new GitCommitRef { CommitId = "4a92b977" }, + LastMergeTargetCommit = new GitCommitRef { CommitId = "78a3c113" } + }); + + mock.Setup(arg => arg.GetPullRequestsAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + null, + null, + 1, + null, + default(CancellationToken))) + .ReturnsAsync((string project2, string repoId2, GitPullRequestSearchCriteria sc, int j1, int j2, int top, object o2, CancellationToken c2) + => new List(new[] + { + new GitPullRequest + { + PullRequestId = 777, + Repository = new GitRepository + { + Id = Guid.NewGuid(), + Name = repoId2 + }, + SourceRefName = sc.SourceRefName, + TargetRefName = "master", + CodeReviewId = 123, + LastMergeSourceCommit = new GitCommitRef { CommitId = "4a92b977" }, + LastMergeTargetCommit = new GitCommitRef { CommitId = "78a3c113" } + } + })); + + mock = this.Setup(mock); + + return mock.Object; + } + + protected override Mock Setup(Mock m) + { + m.Setup(arg => arg.CreatePullRequestReviewerAsync( + It.Is(i => Enum.IsDefined(typeof(TfsPullRequestVote), i.Vote)), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + default(CancellationToken))) + .ReturnsAsync((IdentityRefWithVote identity, Guid project, int prId, string reviewerId, object o, CancellationToken c) + => new IdentityRefWithVote + { + Vote = identity.Vote, + }); + + m.Setup(arg => arg.CreatePullRequestReviewerAsync( + It.Is(i => !Enum.IsDefined(typeof(TfsPullRequestVote), i.Vote)), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + default(CancellationToken))) + .Throws(new Exception("Something went wrong")); + + m.Setup(arg => arg.CreatePullRequestStatusAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync((GitPullRequestStatus status, Guid repoId, int prId, object o, CancellationToken c) + => new GitPullRequestStatus + { + Context = status.Context, + State = status.State + }); + + return m; + } + } +} diff --git a/src/Cake.Tfs.Tests/PullRequest/Fakes/FakeGitClientFactory.cs b/src/Cake.Tfs.Tests/PullRequest/Fakes/FakeGitClientFactory.cs new file mode 100644 index 00000000..623372d8 --- /dev/null +++ b/src/Cake.Tfs.Tests/PullRequest/Fakes/FakeGitClientFactory.cs @@ -0,0 +1,24 @@ +namespace Cake.Tfs.Tests.PullRequest.Fakes +{ + using System; + using Cake.Tfs.Authentication; + using Microsoft.TeamFoundation.SourceControl.WebApi; + using Microsoft.VisualStudio.Services.Identity; + using Moq; + + public abstract class FakeGitClientFactory : IGitClientFactory + { + public abstract GitHttpClient CreateGitClient(Uri collectionUrl, ITfsCredentials credentials); + + public GitHttpClient CreateGitClient(Uri collectionUrl, ITfsCredentials credentials, out Identity identity) + { + identity = new Identity { ProviderDisplayName = "FakeUser", Id = Guid.NewGuid(), IsActive = true }; + return this.CreateGitClient(collectionUrl, credentials); + } + + protected virtual Mock Setup(Mock m) + { + return m; + } + } +} diff --git a/src/Cake.Tfs.Tests/PullRequest/Fakes/FakeNullForMethodsGitClientFactory.cs b/src/Cake.Tfs.Tests/PullRequest/Fakes/FakeNullForMethodsGitClientFactory.cs new file mode 100644 index 00000000..532f83a2 --- /dev/null +++ b/src/Cake.Tfs.Tests/PullRequest/Fakes/FakeNullForMethodsGitClientFactory.cs @@ -0,0 +1,21 @@ +namespace Cake.Tfs.Tests.PullRequest.Fakes +{ + using System; + using System.Threading; + using Microsoft.TeamFoundation.SourceControl.WebApi; + using Moq; + + public class FakeNullForMethodsGitClientFactory : FakeAllSetGitClientFactory + { + protected override Mock Setup(Mock m) + { + m.Setup(arg => arg.CreatePullRequestReviewerAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), default(CancellationToken))) + .ReturnsAsync(() => null); + + m.Setup(arg => arg.CreatePullRequestStatusAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(() => null); + + return m; + } + } +} diff --git a/src/Cake.Tfs.Tests/PullRequest/Fakes/FakeNullGitClientFactory.cs b/src/Cake.Tfs.Tests/PullRequest/Fakes/FakeNullGitClientFactory.cs new file mode 100644 index 00000000..4cb728ac --- /dev/null +++ b/src/Cake.Tfs.Tests/PullRequest/Fakes/FakeNullGitClientFactory.cs @@ -0,0 +1,27 @@ +namespace Cake.Tfs.Tests.PullRequest.Fakes +{ + using System; + using System.Collections.Generic; + using System.Threading; + using Cake.Tfs.Authentication; + using Microsoft.TeamFoundation.SourceControl.WebApi; + using Moq; + + public class FakeNullGitClientFactory : FakeGitClientFactory + { + public override GitHttpClient CreateGitClient(Uri collectionUrl, ITfsCredentials credentials) + { + var mock = new Mock(MockBehavior.Loose, collectionUrl, credentials.ToVssCredentials()); + + mock.Setup(arg => arg.GetPullRequestAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, null, null, null, null, null, default(CancellationToken))) + .ReturnsAsync(() => null); + + mock.Setup(arg => arg.GetPullRequestsAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, null, 1, null, default(CancellationToken))) + .ReturnsAsync(() => new List()); + + mock = this.Setup(mock); + + return mock.Object; + } + } +} diff --git a/src/Cake.Tfs.Tests/PullRequest/PullRequestFixture.cs b/src/Cake.Tfs.Tests/PullRequest/PullRequestFixture.cs new file mode 100644 index 00000000..0dc94bbf --- /dev/null +++ b/src/Cake.Tfs.Tests/PullRequest/PullRequestFixture.cs @@ -0,0 +1,41 @@ +namespace Cake.Tfs.Tests.PullRequest +{ + using System; + using Cake.Testing; + using Cake.Tfs.Authentication; + using Cake.Tfs.PullRequest; + using Cake.Tfs.Tests.PullRequest.Fakes; + + internal class PullRequestFixture + { + public const string ValidTfsUrl = "http://MyServer/tfs/MyCollection/MyTeamProject/_git/MyRepoName"; + public const string ValidAzureDevOpsUrl = "https://my-account.visualstudio.com/DefaultCollection/MyProject/_git/MyRepoName"; + public const string InvalidTfsUrl = "http://example.com"; + + public PullRequestFixture(string repoUrl, int prId) + { + this.Settings = new TfsPullRequestSettings(new Uri(repoUrl), prId, new TfsNtlmCredentials()); + + this.InitializeFakes(); + } + + public PullRequestFixture(string repoUrl, string sourceBranch) + { + this.Settings = new TfsPullRequestSettings(new Uri(repoUrl), sourceBranch, new TfsNtlmCredentials()); + + this.InitializeFakes(); + } + + public FakeLog Log { get; set; } + + public TfsPullRequestSettings Settings { get; set; } + + public IGitClientFactory GitClientFactory { get; set; } + + private void InitializeFakes() + { + this.Log = new FakeLog(); + this.GitClientFactory = new FakeAllSetGitClientFactory(); + } + } +} diff --git a/src/Cake.Tfs.Tests/PullRequest/TfsPullRequestTests.cs b/src/Cake.Tfs.Tests/PullRequest/TfsPullRequestTests.cs index 4385bfbd..cb09c69d 100644 --- a/src/Cake.Tfs.Tests/PullRequest/TfsPullRequestTests.cs +++ b/src/Cake.Tfs.Tests/PullRequest/TfsPullRequestTests.cs @@ -1,10 +1,8 @@ namespace Cake.Tfs.Tests.PullRequest { - using System; - using Cake.Core.Diagnostics; - using Cake.Testing; - using Cake.Tfs.Authentication; using Cake.Tfs.PullRequest; + using Cake.Tfs.Tests.PullRequest.Fakes; + using Microsoft.VisualStudio.Services.Common; using Shouldly; using Xunit; @@ -16,11 +14,10 @@ public sealed class TheCtor public void Should_Throw_If_Log_Is_Null() { // Given - ICakeLog log = null; - var settings = new TfsPullRequestSettings(new Uri("http://example.com"), "foo", AuthenticationProvider.AuthenticationNtlm()); + var fixture = new PullRequestFixture(PullRequestFixture.ValidTfsUrl, "foo") { Log = null }; // When - var result = Record.Exception(() => new TfsPullRequest(log, settings)); + var result = Record.Exception(() => new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory)); // Then result.IsArgumentNullException("log"); @@ -30,15 +27,389 @@ public void Should_Throw_If_Log_Is_Null() public void Should_Throw_If_Settings_Are_Null() { // Given - var log = new FakeLog(); - TfsPullRequestSettings settings = null; + var fixture = new PullRequestFixture(PullRequestFixture.ValidTfsUrl, 42) { Settings = null }; // When - var result = Record.Exception(() => new TfsPullRequest(log, settings)); + var result = Record.Exception(() => new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory)); // Then result.IsArgumentNullException("settings"); } + + [Fact] + public void Should_Throw_If_Git_Client_Factory_Is_Null() + { + // Given + var fixture = new PullRequestFixture(PullRequestFixture.ValidTfsUrl, 42) { GitClientFactory = null }; + + // When + var result = Record.Exception(() => new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory)); + + // Then + result.IsArgumentNullException("gitClientFactory"); + } + + [Fact] + public void Should_Throw_If_Tfs_Url_Is_Invalid() + { + // Given + var fixture = new PullRequestFixture(PullRequestFixture.InvalidTfsUrl, 42); + + // When + var result = Record.Exception(() => new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory)); + + // Then + result.IsUrlFormatException(); + } + + [Fact] + public void Should_Return_Valid_Tfs_Pull_Request_By_Id() + { + // Given + var fixture = new PullRequestFixture(PullRequestFixture.ValidTfsUrl, 42); + + // When + var pullRequest = new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory); + + // Then + pullRequest.ShouldNotBe(null); + pullRequest.HasPullRequestLoaded.ShouldBe(true); + pullRequest.PullRequestId.ShouldBe(42); + pullRequest.RepositoryName.ShouldBe("MyRepoName"); + pullRequest.CollectionName.ShouldBe("MyCollection"); + pullRequest.CodeReviewId.ShouldBe(123); + pullRequest.ProjectName.ShouldBe("MyTeamProject"); + pullRequest.TargetRefName.ShouldBe("master"); + pullRequest.LastSourceCommitId.ShouldBe("4a92b977"); + pullRequest.LastTargetCommitId.ShouldBe("78a3c113"); + } + + [Fact] + public void Should_Return_Valid_Azure_DevOps_Pull_Request_By_Id() + { + // Given + var fixture = new PullRequestFixture(PullRequestFixture.ValidAzureDevOpsUrl, 16); + + // When + var pullRequest = new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory); + + // Then + pullRequest.ShouldNotBe(null); + pullRequest.HasPullRequestLoaded.ShouldBe(true); + pullRequest.PullRequestId.ShouldBe(16); + pullRequest.RepositoryName.ShouldBe("MyRepoName"); + pullRequest.CollectionName.ShouldBe("DefaultCollection"); + pullRequest.CodeReviewId.ShouldBe(123); + pullRequest.ProjectName.ShouldBe("MyProject"); + pullRequest.TargetRefName.ShouldBe("master"); + pullRequest.LastSourceCommitId.ShouldBe("4a92b977"); + pullRequest.LastTargetCommitId.ShouldBe("78a3c113"); + } + + [Fact] + public void Should_Return_Valid_Tfs_Pull_Request_By_Source_Branch() + { + // Given + var fixture = new PullRequestFixture(PullRequestFixture.ValidTfsUrl, "feature"); + + // When + var pullRequest = new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory); + + // Then + pullRequest.ShouldNotBe(null); + pullRequest.HasPullRequestLoaded.ShouldBe(true); + pullRequest.PullRequestId.ShouldBe(777); + pullRequest.RepositoryName.ShouldBe("MyRepoName"); + pullRequest.CollectionName.ShouldBe("MyCollection"); + pullRequest.CodeReviewId.ShouldBe(123); + pullRequest.ProjectName.ShouldBe("MyTeamProject"); + pullRequest.TargetRefName.ShouldBe("master"); + pullRequest.LastSourceCommitId.ShouldBe("4a92b977"); + pullRequest.LastTargetCommitId.ShouldBe("78a3c113"); + } + + [Fact] + public void Should_Return_Valid_Azure_DevOps_Pull_Request_By_Source_Branch() + { + // Given + var fixture = new PullRequestFixture(PullRequestFixture.ValidAzureDevOpsUrl, "feature"); + + // When + var pullRequest = new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory); + + // Then + pullRequest.ShouldNotBe(null); + pullRequest.HasPullRequestLoaded.ShouldBe(true); + pullRequest.PullRequestId.ShouldBe(777); + pullRequest.RepositoryName.ShouldBe("MyRepoName"); + pullRequest.CollectionName.ShouldBe("DefaultCollection"); + pullRequest.CodeReviewId.ShouldBe(123); + pullRequest.ProjectName.ShouldBe("MyProject"); + pullRequest.TargetRefName.ShouldBe("master"); + pullRequest.LastSourceCommitId.ShouldBe("4a92b977"); + pullRequest.LastTargetCommitId.ShouldBe("78a3c113"); + } + + [Fact] + public void Should_Return_Null_Tfs_Pull_Request_By_Id() + { + // Given + var fixture = + new PullRequestFixture(PullRequestFixture.ValidTfsUrl, 101) + { + GitClientFactory = new FakeNullGitClientFactory(), + Settings = { ThrowExceptionIfPullRequestCouldNotBeFound = false } + }; + + // When + var pullRequest = new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory); + + // Then + pullRequest.ShouldNotBe(null); + pullRequest.HasPullRequestLoaded.ShouldBe(false); + pullRequest.RepositoryName.ShouldBe("MyRepoName"); + pullRequest.CollectionName.ShouldBe("MyCollection"); + pullRequest.ProjectName.ShouldBe("MyTeamProject"); + pullRequest.PullRequestId.ShouldBe(0); + pullRequest.CodeReviewId.ShouldBe(0); + pullRequest.TargetRefName.ShouldBeEmpty(); + pullRequest.LastSourceCommitId.ShouldBeEmpty(); + pullRequest.LastTargetCommitId.ShouldBeEmpty(); + } + + [Fact] + public void Should_Return_Null_Azure_DevOps_Pull_Request_By_Id() + { + // Given + var fixture = + new PullRequestFixture(PullRequestFixture.ValidAzureDevOpsUrl, 101) + { + GitClientFactory = new FakeNullGitClientFactory(), + Settings = { ThrowExceptionIfPullRequestCouldNotBeFound = false } + }; + + // When + var pullRequest = new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory); + + // Then + pullRequest.ShouldNotBe(null); + pullRequest.HasPullRequestLoaded.ShouldBe(false); + pullRequest.RepositoryName.ShouldBe("MyRepoName"); + pullRequest.CollectionName.ShouldBe("DefaultCollection"); + pullRequest.ProjectName.ShouldBe("MyProject"); + pullRequest.PullRequestId.ShouldBe(0); + pullRequest.CodeReviewId.ShouldBe(0); + pullRequest.TargetRefName.ShouldBeEmpty(); + pullRequest.LastSourceCommitId.ShouldBeEmpty(); + pullRequest.LastTargetCommitId.ShouldBeEmpty(); + } + + [Fact] + public void Should_Return_Null_Tfs_Pull_Request_By_Branch() + { + // Given + var fixture = + new PullRequestFixture(PullRequestFixture.ValidTfsUrl, "somebranch") + { + GitClientFactory = new FakeNullGitClientFactory(), + Settings = { ThrowExceptionIfPullRequestCouldNotBeFound = false } + }; + + // When + var pullRequest = new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory); + + // Then + pullRequest.ShouldNotBe(null); + pullRequest.HasPullRequestLoaded.ShouldBe(false); + pullRequest.RepositoryName.ShouldBe("MyRepoName"); + pullRequest.CollectionName.ShouldBe("MyCollection"); + pullRequest.ProjectName.ShouldBe("MyTeamProject"); + pullRequest.PullRequestId.ShouldBe(0); + pullRequest.CodeReviewId.ShouldBe(0); + pullRequest.TargetRefName.ShouldBeEmpty(); + pullRequest.LastSourceCommitId.ShouldBeEmpty(); + pullRequest.LastTargetCommitId.ShouldBeEmpty(); + } + + [Fact] + public void Should_Return_Null_Azure_DevOps_Pull_Request_By_Branch() + { + // Given + var fixture = + new PullRequestFixture(PullRequestFixture.ValidAzureDevOpsUrl, "somebranch") + { + GitClientFactory = new FakeNullGitClientFactory(), + Settings = { ThrowExceptionIfPullRequestCouldNotBeFound = false } + }; + + // When + var pullRequest = new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory); + + // Then + pullRequest.ShouldNotBe(null); + pullRequest.HasPullRequestLoaded.ShouldBe(false); + pullRequest.RepositoryName.ShouldBe("MyRepoName"); + pullRequest.CollectionName.ShouldBe("DefaultCollection"); + pullRequest.ProjectName.ShouldBe("MyProject"); + pullRequest.PullRequestId.ShouldBe(0); + pullRequest.CodeReviewId.ShouldBe(0); + pullRequest.TargetRefName.ShouldBeEmpty(); + pullRequest.LastSourceCommitId.ShouldBeEmpty(); + pullRequest.LastTargetCommitId.ShouldBeEmpty(); + } + + [Fact] + public void Should_Throw_If_Strict_Is_On_And_Pull_Request_Is_Null_By_Id() + { + // Given + var fixture = + new PullRequestFixture(PullRequestFixture.ValidTfsUrl, 1) + { + GitClientFactory = new FakeNullGitClientFactory() + }; + + // When + var result = Record.Exception(() => new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory)); + + // Then + result.IsTfsPullRequestNotFoundException(); + } + + [Fact] + public void Should_Throw_If_Strict_Is_On_And_Pull_Request_Is_Null_By_Branch() + { + // Given + var fixture = + new PullRequestFixture(PullRequestFixture.ValidTfsUrl, "feature") + { + GitClientFactory = new FakeNullGitClientFactory() + }; + + // When + var result = Record.Exception(() => new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory)); + + // Then + result.IsTfsPullRequestNotFoundException(); + } + } + + public sealed class Vote + { + [Fact] + public void Should_Set_Approved_Vote_On_Tfs_Pull_Request() + { + // Given + var fixture = new PullRequestFixture(PullRequestFixture.ValidTfsUrl, 23); + var pullRequest = new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory); + + // When + pullRequest.Vote(TfsPullRequestVote.Approved); + + // Then + // ?? Nothing to validate here since the method returns void + } + + [Fact] + public void Should_Throw_If_Vote_Value_Is_Invalid_On_Tfs_Pull_Request() + { + // Given + var fixture = new PullRequestFixture(PullRequestFixture.ValidTfsUrl, 23); + var pullRequest = new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory); + + // When + var result = Record.Exception(() => pullRequest.Vote((TfsPullRequestVote)3)); + + // Then + result.ShouldNotBe(null); + result.IsExpected("Vote"); + result.Message.ShouldBe("Something went wrong"); + } + + [Fact] + public void Should_Throw_If_Null_Is_Returned_On_Tfs_Pull_Request() + { + // Given + var fixture = new PullRequestFixture(PullRequestFixture.ValidTfsUrl, 23) + { + GitClientFactory = new FakeNullForMethodsGitClientFactory() + }; + var pullRequest = new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory); + + // When + var result = Record.Exception(() => pullRequest.Vote(TfsPullRequestVote.WaitingForAuthor)); + + // Then + result.ShouldNotBe(null); + result.IsExpected("Vote"); + } + } + + public sealed class SetStatus + { + [Fact] + public void Should_Throw_If_Tfs_Pull_Request_Status_Is_Null() + { + // Given + var fixture = new PullRequestFixture(PullRequestFixture.ValidTfsUrl, 16); + var pullRequest = new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory); + + // When + var result = Record.Exception(() => pullRequest.SetStatus(null)); + + // Then + result.IsArgumentNullException("status"); + } + + [Fact] + public void Should_Throw_If_Tfs_Pull_Request_State_Is_Invalid() + { + // Given + var fixture = new PullRequestFixture(PullRequestFixture.ValidTfsUrl, 16); + var pullRequest = new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory); + var status = new TfsPullRequestStatus("whatever") { State = (TfsPullRequestStatusState)123 }; + + // When + var result = Record.Exception(() => pullRequest.SetStatus(status)); + + // Then + result.ShouldNotBe(null); + result.IsExpected("SetStatus"); + result.Message.ShouldBe("Unknown value"); + } + + [Fact] + public void Should_Set_Valid_Status_On_Tfs_Pull_Request() + { + // Given + var fixture = new PullRequestFixture(PullRequestFixture.ValidTfsUrl, 16); + var pullRequest = new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory); + var status = new TfsPullRequestStatus("Hello") { State = TfsPullRequestStatusState.Succeeded }; + + // When + pullRequest.SetStatus(status); + + // Then + // ?? Nothing to validate here since the method returns void + } + + [Fact] + public void Should_Throw_If_Null_Is_Returned_On_Tfs_Pull_Request() + { + // Given + var fixture = new PullRequestFixture(PullRequestFixture.ValidTfsUrl, 16) + { + GitClientFactory = new FakeNullForMethodsGitClientFactory() + }; + var pullRequest = new TfsPullRequest(fixture.Log, fixture.Settings, fixture.GitClientFactory); + var status = new TfsPullRequestStatus("One") { State = TfsPullRequestStatusState.Failed }; + + // When + var result = Record.Exception(() => pullRequest.SetStatus(status)); + + // Then + result.ShouldNotBe(null); + result.IsExpected("SetStatus"); + } } } } diff --git a/src/Cake.Tfs/PullRequest/GitClient/GitClientFactory.cs b/src/Cake.Tfs/PullRequest/GitClient/GitClientFactory.cs new file mode 100644 index 00000000..1c917877 --- /dev/null +++ b/src/Cake.Tfs/PullRequest/GitClient/GitClientFactory.cs @@ -0,0 +1,46 @@ +namespace Cake.Tfs +{ + using System; + using Cake.Tfs.Authentication; + using Microsoft.TeamFoundation.SourceControl.WebApi; + using Microsoft.VisualStudio.Services.Identity; + using Microsoft.VisualStudio.Services.WebApi; + + /// + internal class GitClientFactory : IGitClientFactory + { + /// + /// Creates a client object for communicating with Team Foundation Server or Azure DevOps. + /// + /// The URL of the TFS/Azure DevOps team project collection. + /// The credentials to connect to TFS/Azure DevOps. + /// Returns identity which is authorized. + /// Client object for communicating with Team Foundation Server or Azure DevOps + public GitHttpClient CreateGitClient(Uri collectionUrl, ITfsCredentials credentials, out Identity authorizedIdentity) + { + var connection = + new VssConnection(collectionUrl, credentials.ToVssCredentials()); + + authorizedIdentity = connection.AuthorizedIdentity; + + var gitClient = connection.GetClient(); + if (gitClient == null) + { + throw new TfsException("Could not retrieve the GitHttpClient object"); + } + + return gitClient; + } + + /// + /// Creates a client object for communicating with Team Foundation Server or Azure DevOps. + /// + /// The URL of the TFS/Azure DevOps team project collection. + /// The credentials to connect to TFS/Azure DevOps. + /// Client object for communicating with Team Foundation Server or Azure DevOps + public GitHttpClient CreateGitClient(Uri collectionUrl, ITfsCredentials credentials) + { + return this.CreateGitClient(collectionUrl, credentials, out _); + } + } +} diff --git a/src/Cake.Tfs/PullRequest/GitClient/IGitClientFactory.cs b/src/Cake.Tfs/PullRequest/GitClient/IGitClientFactory.cs new file mode 100644 index 00000000..a181d5b7 --- /dev/null +++ b/src/Cake.Tfs/PullRequest/GitClient/IGitClientFactory.cs @@ -0,0 +1,30 @@ +namespace Cake.Tfs +{ + using System; + using Cake.Tfs.Authentication; + using Microsoft.TeamFoundation.SourceControl.WebApi; + using Microsoft.VisualStudio.Services.Identity; + + /// + /// The interface for a Git client factory + /// + public interface IGitClientFactory + { + /// + /// Creates the instance of the class. + /// + /// The URL of the TFS/Azure DevOps team project collection. + /// The credentials to connect to TFS/Azure DevOps. + /// The instance of class. + GitHttpClient CreateGitClient(Uri collectionUrl, ITfsCredentials credentials); + + /// + /// Creates the instance of the class. + /// + /// The URL of the TFS/Azure DevOps team project collection. + /// The credentials to connect to TFS/Azure DevOps. + /// Returns identity which is authorized. + /// The instance of class. + GitHttpClient CreateGitClient(Uri collectionUrl, ITfsCredentials credentials, out Identity identity); + } +} diff --git a/src/Cake.Tfs/PullRequest/TfsPullRequest.cs b/src/Cake.Tfs/PullRequest/TfsPullRequest.cs index e6ff67ff..5960947a 100644 --- a/src/Cake.Tfs/PullRequest/TfsPullRequest.cs +++ b/src/Cake.Tfs/PullRequest/TfsPullRequest.cs @@ -4,10 +4,8 @@ using System.Linq; using System.Threading; using Cake.Core.Diagnostics; - using Cake.Tfs.Authentication; + using Cake.Tfs; using Microsoft.TeamFoundation.SourceControl.WebApi; - using Microsoft.VisualStudio.Services.Identity; - using Microsoft.VisualStudio.Services.WebApi; using TfsUrlParser; /// @@ -17,6 +15,7 @@ public sealed class TfsPullRequest { private readonly ICakeLog log; private readonly TfsPullRequestSettings settings; + private readonly IGitClientFactory gitClientFactory; private readonly RepositoryDescription repositoryDescription; private readonly GitPullRequest pullRequest; @@ -25,15 +24,18 @@ public sealed class TfsPullRequest /// /// The Cake log context. /// Settings for accessing TFS. + /// A factory to communicate with Git client. /// If /// is set to true and no pull request could be found. - public TfsPullRequest(ICakeLog log, TfsPullRequestSettings settings) + public TfsPullRequest(ICakeLog log, TfsPullRequestSettings settings, IGitClientFactory gitClientFactory) { log.NotNull(nameof(log)); settings.NotNull(nameof(settings)); + gitClientFactory.NotNull(nameof(gitClientFactory)); this.log = log; this.settings = settings; + this.gitClientFactory = gitClientFactory; this.repositoryDescription = new RepositoryDescription(settings.RepositoryUrl); @@ -44,7 +46,7 @@ public TfsPullRequest(ICakeLog log, TfsPullRequestSettings settings) this.repositoryDescription.ProjectName, this.repositoryDescription.RepositoryName); - using (var gitClient = this.CreateGitClient(out var authorizedIdenity)) + using (var gitClient = this.gitClientFactory.CreateGitClient(this.repositoryDescription.CollectionUrl, settings.Credentials, out var authorizedIdenity)) { this.log.Verbose( "Authorized Identity:\n Id: {0}\n DisplayName: {1}", @@ -269,7 +271,7 @@ public void Vote(TfsPullRequestVote vote) return; } - using (var gitClient = this.CreateGitClient(out var authorizedIdenity)) + using (var gitClient = this.gitClientFactory.CreateGitClient(this.CollectionUrl, this.settings.Credentials, out var authorizedIdenity)) { var request = gitClient.CreatePullRequestReviewerAsync( @@ -308,7 +310,7 @@ public void SetStatus(TfsPullRequestStatus status) return; } - using (var gitClient = this.CreateGitClient()) + using (var gitClient = this.gitClientFactory.CreateGitClient(this.CollectionUrl, this.settings.Credentials)) { var request = gitClient.CreatePullRequestStatusAsync( @@ -365,37 +367,5 @@ private bool ValidatePullRequest() this.log.Verbose("Skipping, since no pull request instance could be found."); return false; } - - /// - /// Creates a client object for communicating with Team Foundation Server or Azure DevOps. - /// - /// Returns identity which is authorized. - /// Client object for communicating with Team Foundation Server or Azure DevOps - private GitHttpClient CreateGitClient(out Identity authorizedIdentity) - { - var connection = - new VssConnection( - this.repositoryDescription.CollectionUrl, - this.settings.Credentials.ToVssCredentials()); - - authorizedIdentity = connection.AuthorizedIdentity; - - var gitClient = connection.GetClient(); - if (gitClient == null) - { - throw new TfsException("Could not retrieve the GitHttpClient object"); - } - - return gitClient; - } - - /// - /// Creates a client object for communicating with Team Foundation Server or Azure DevOps. - /// - /// Client object for communicating with Team Foundation Server or Azure DevOps - private GitHttpClient CreateGitClient() - { - return this.CreateGitClient(out var identity); - } } } diff --git a/src/Cake.Tfs/TfsAliases.PullRequest.cs b/src/Cake.Tfs/TfsAliases.PullRequest.cs index d1d5de50..4bf6a07a 100644 --- a/src/Cake.Tfs/TfsAliases.PullRequest.cs +++ b/src/Cake.Tfs/TfsAliases.PullRequest.cs @@ -46,7 +46,7 @@ public static TfsPullRequest TfsPullRequest( context.NotNull(nameof(context)); settings.NotNull(nameof(settings)); - var pullRequest = new TfsPullRequest(context.Log, settings); + var pullRequest = new TfsPullRequest(context.Log, settings, new GitClientFactory()); if (pullRequest.HasPullRequestLoaded) { @@ -125,7 +125,7 @@ public static void TfsVotePullRequest( context.NotNull(nameof(context)); settings.NotNull(nameof(settings)); - new TfsPullRequest(context.Log, settings).Vote(vote); + new TfsPullRequest(context.Log, settings, new GitClientFactory()).Vote(vote); } /// @@ -172,7 +172,7 @@ public static void TfsSetPullRequestStatus( settings.NotNull(nameof(settings)); status.NotNull(nameof(status)); - new TfsPullRequest(context.Log, settings).SetStatus(status); + new TfsPullRequest(context.Log, settings, new GitClientFactory()).SetStatus(status); } } } \ No newline at end of file From b7963d68553d80e1884f905cec7eea1d81cf8f69 Mon Sep 17 00:00:00 2001 From: Pascal Berger Date: Sun, 21 Oct 2018 22:02:04 +0200 Subject: [PATCH 2/5] (GH-37) Throw specific exception in case pull request could not be found --- .../PullRequest/TfsPullRequestTests.cs | 2 ++ src/Cake.Tfs/PullRequest/TfsPullRequest.cs | 16 ++++++++++++++++ .../TfsPullRequestNotFoundException.cs | 10 ++++++++++ 3 files changed, 28 insertions(+) diff --git a/src/Cake.Tfs.Tests/PullRequest/TfsPullRequestTests.cs b/src/Cake.Tfs.Tests/PullRequest/TfsPullRequestTests.cs index cb09c69d..71520c2d 100644 --- a/src/Cake.Tfs.Tests/PullRequest/TfsPullRequestTests.cs +++ b/src/Cake.Tfs.Tests/PullRequest/TfsPullRequestTests.cs @@ -341,6 +341,7 @@ public void Should_Throw_If_Null_Is_Returned_On_Tfs_Pull_Request() // Then result.ShouldNotBe(null); result.IsExpected("Vote"); + result.IsTfsPullRequestNotFoundException(); } } @@ -409,6 +410,7 @@ public void Should_Throw_If_Null_Is_Returned_On_Tfs_Pull_Request() // Then result.ShouldNotBe(null); result.IsExpected("SetStatus"); + result.IsTfsPullRequestNotFoundException(); } } } diff --git a/src/Cake.Tfs/PullRequest/TfsPullRequest.cs b/src/Cake.Tfs/PullRequest/TfsPullRequest.cs index 5960947a..04bc550c 100644 --- a/src/Cake.Tfs/PullRequest/TfsPullRequest.cs +++ b/src/Cake.Tfs/PullRequest/TfsPullRequest.cs @@ -284,6 +284,14 @@ public void Vote(TfsPullRequestVote vote) try { var createdReviewer = request.Result; + + if (createdReviewer == null) + { + throw new TfsPullRequestNotFoundException( + this.pullRequest.Repository.Id, + this.pullRequest.PullRequestId); + } + var createdVote = (TfsPullRequestVote)createdReviewer.Vote; this.log.Verbose("Voted for pull request with '{0}'.", createdVote.ToString()); } @@ -331,6 +339,14 @@ public void SetStatus(TfsPullRequestStatus status) try { var postedStatus = request.Result; + + if (postedStatus == null) + { + throw new TfsPullRequestNotFoundException( + this.pullRequest.Repository.Id, + this.pullRequest.PullRequestId); + } + this.log.Verbose( "Set status '{0}' to {1}.", postedStatus.Context?.Name, diff --git a/src/Cake.Tfs/PullRequest/TfsPullRequestNotFoundException.cs b/src/Cake.Tfs/PullRequest/TfsPullRequestNotFoundException.cs index b3eb35dc..bbdea580 100644 --- a/src/Cake.Tfs/PullRequest/TfsPullRequestNotFoundException.cs +++ b/src/Cake.Tfs/PullRequest/TfsPullRequestNotFoundException.cs @@ -16,6 +16,16 @@ public TfsPullRequestNotFoundException() { } + /// + /// Initializes a new instance of the class. + /// + /// ID of the repository where the pull request was searched. + /// ID of the pull request which could not be found. + public TfsPullRequestNotFoundException(Guid repositoryId, int pullRequestId) + : this("Pull request with ID " + pullRequestId + " not found in repository with GUID " + repositoryId) + { + } + /// /// Initializes a new instance of the class with a specified error message. /// From 2d46eb26dc7785d7fbab446153231d848328c529 Mon Sep 17 00:00:00 2001 From: Yan Sklyarenko Date: Sat, 6 Oct 2018 23:12:37 +0300 Subject: [PATCH 3/5] Return source branch for pull request --- .../PullRequest/TfsPullRequestTests.cs | 8 ++++++++ src/Cake.Tfs/PullRequest/TfsPullRequest.cs | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Cake.Tfs.Tests/PullRequest/TfsPullRequestTests.cs b/src/Cake.Tfs.Tests/PullRequest/TfsPullRequestTests.cs index 71520c2d..307dc112 100644 --- a/src/Cake.Tfs.Tests/PullRequest/TfsPullRequestTests.cs +++ b/src/Cake.Tfs.Tests/PullRequest/TfsPullRequestTests.cs @@ -79,6 +79,7 @@ public void Should_Return_Valid_Tfs_Pull_Request_By_Id() pullRequest.CollectionName.ShouldBe("MyCollection"); pullRequest.CodeReviewId.ShouldBe(123); pullRequest.ProjectName.ShouldBe("MyTeamProject"); + pullRequest.SourceRefName.ShouldBe("foo"); pullRequest.TargetRefName.ShouldBe("master"); pullRequest.LastSourceCommitId.ShouldBe("4a92b977"); pullRequest.LastTargetCommitId.ShouldBe("78a3c113"); @@ -101,6 +102,7 @@ public void Should_Return_Valid_Azure_DevOps_Pull_Request_By_Id() pullRequest.CollectionName.ShouldBe("DefaultCollection"); pullRequest.CodeReviewId.ShouldBe(123); pullRequest.ProjectName.ShouldBe("MyProject"); + pullRequest.SourceRefName.ShouldBe("foo"); pullRequest.TargetRefName.ShouldBe("master"); pullRequest.LastSourceCommitId.ShouldBe("4a92b977"); pullRequest.LastTargetCommitId.ShouldBe("78a3c113"); @@ -123,6 +125,7 @@ public void Should_Return_Valid_Tfs_Pull_Request_By_Source_Branch() pullRequest.CollectionName.ShouldBe("MyCollection"); pullRequest.CodeReviewId.ShouldBe(123); pullRequest.ProjectName.ShouldBe("MyTeamProject"); + pullRequest.SourceRefName.ShouldBe("feature"); pullRequest.TargetRefName.ShouldBe("master"); pullRequest.LastSourceCommitId.ShouldBe("4a92b977"); pullRequest.LastTargetCommitId.ShouldBe("78a3c113"); @@ -145,6 +148,7 @@ public void Should_Return_Valid_Azure_DevOps_Pull_Request_By_Source_Branch() pullRequest.CollectionName.ShouldBe("DefaultCollection"); pullRequest.CodeReviewId.ShouldBe(123); pullRequest.ProjectName.ShouldBe("MyProject"); + pullRequest.SourceRefName.ShouldBe("feature"); pullRequest.TargetRefName.ShouldBe("master"); pullRequest.LastSourceCommitId.ShouldBe("4a92b977"); pullRequest.LastTargetCommitId.ShouldBe("78a3c113"); @@ -172,6 +176,7 @@ public void Should_Return_Null_Tfs_Pull_Request_By_Id() pullRequest.ProjectName.ShouldBe("MyTeamProject"); pullRequest.PullRequestId.ShouldBe(0); pullRequest.CodeReviewId.ShouldBe(0); + pullRequest.SourceRefName.ShouldBeEmpty(); pullRequest.TargetRefName.ShouldBeEmpty(); pullRequest.LastSourceCommitId.ShouldBeEmpty(); pullRequest.LastTargetCommitId.ShouldBeEmpty(); @@ -199,6 +204,7 @@ public void Should_Return_Null_Azure_DevOps_Pull_Request_By_Id() pullRequest.ProjectName.ShouldBe("MyProject"); pullRequest.PullRequestId.ShouldBe(0); pullRequest.CodeReviewId.ShouldBe(0); + pullRequest.SourceRefName.ShouldBeEmpty(); pullRequest.TargetRefName.ShouldBeEmpty(); pullRequest.LastSourceCommitId.ShouldBeEmpty(); pullRequest.LastTargetCommitId.ShouldBeEmpty(); @@ -226,6 +232,7 @@ public void Should_Return_Null_Tfs_Pull_Request_By_Branch() pullRequest.ProjectName.ShouldBe("MyTeamProject"); pullRequest.PullRequestId.ShouldBe(0); pullRequest.CodeReviewId.ShouldBe(0); + pullRequest.SourceRefName.ShouldBeEmpty(); pullRequest.TargetRefName.ShouldBeEmpty(); pullRequest.LastSourceCommitId.ShouldBeEmpty(); pullRequest.LastTargetCommitId.ShouldBeEmpty(); @@ -253,6 +260,7 @@ public void Should_Return_Null_Azure_DevOps_Pull_Request_By_Branch() pullRequest.ProjectName.ShouldBe("MyProject"); pullRequest.PullRequestId.ShouldBe(0); pullRequest.CodeReviewId.ShouldBe(0); + pullRequest.SourceRefName.ShouldBeEmpty(); pullRequest.TargetRefName.ShouldBeEmpty(); pullRequest.LastSourceCommitId.ShouldBeEmpty(); pullRequest.LastTargetCommitId.ShouldBeEmpty(); diff --git a/src/Cake.Tfs/PullRequest/TfsPullRequest.cs b/src/Cake.Tfs/PullRequest/TfsPullRequest.cs index 04bc550c..013a8c37 100644 --- a/src/Cake.Tfs/PullRequest/TfsPullRequest.cs +++ b/src/Cake.Tfs/PullRequest/TfsPullRequest.cs @@ -198,6 +198,26 @@ public int CodeReviewId } } + /// + /// Gets the name of the source branch + /// + /// Returns if no pull request could be found and + /// is set to false. + /// If pull request could not be found and + /// is set to true. + public string SourceRefName + { + get + { + if (!this.ValidatePullRequest()) + { + return string.Empty; + } + + return this.pullRequest.SourceRefName; + } + } + /// /// Gets the name of the target branch. /// Returns if no pull request could be found and From 30b71f2c1dcb1073673f14f915cf867fadd7786f Mon Sep 17 00:00:00 2001 From: Pascal Berger Date: Sun, 21 Oct 2018 23:51:10 +0200 Subject: [PATCH 4/5] Use jsDelivr for icon --- nuspec/nuget/Cake.Tfs.nuspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuspec/nuget/Cake.Tfs.nuspec b/nuspec/nuget/Cake.Tfs.nuspec index 356acd2b..3bf24500 100644 --- a/nuspec/nuget/Cake.Tfs.nuspec +++ b/nuspec/nuget/Cake.Tfs.nuspec @@ -12,7 +12,7 @@ https://github.com/cake-contrib/Cake.Tfs/blob/develop/LICENSE http://cake-contrib.github.io/Cake.Tfs/ - https://cdn.rawgit.com/cake-contrib/graphics/a5cf0f881c390650144b2243ae551d5b9f836196/png/cake-contrib-medium.png + https://cdn.jsdelivr.net/gh/cake-contrib/graphics@a5cf0f881c390650144b2243ae551d5b9f836196/png/cake-contrib-medium.png false Copyright © Pascal Berger From c368004543803fb9b3add497d9cd5bf6141c5021 Mon Sep 17 00:00:00 2001 From: Pascal Berger Date: Sun, 21 Oct 2018 23:51:29 +0200 Subject: [PATCH 5/5] Update release notes link --- nuspec/nuget/Cake.Tfs.nuspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuspec/nuget/Cake.Tfs.nuspec b/nuspec/nuget/Cake.Tfs.nuspec index 3bf24500..82093414 100644 --- a/nuspec/nuget/Cake.Tfs.nuspec +++ b/nuspec/nuget/Cake.Tfs.nuspec @@ -17,7 +17,7 @@ Copyright © Pascal Berger Cake Script Team-Foundation-Server TFS Azure-DevOps - https://github.com/cake-contrib/Cake.Tfs/releases/tag/0.2.2 + https://github.com/cake-contrib/Cake.Tfs/releases/tag/0.2.3