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
21 changes: 21 additions & 0 deletions GitIntegration.Test/Hosting/AzureDevOpsProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,27 @@
}
}

[TestMethod]
public async Task TranslatesAnUnrecognisedPullRequestStatusToGitHostingRequestExceptionAsync()
{
using FakeHttpMessageHandler handler = new FakeHttpMessageHandler()
.Respond(HttpStatusCode.OK, SinglePullRequestListResponse("notSet"), ("Content-Type", "application/json"));
AzureDevOpsProvider provider = new()
{
Owner = "contoso".As<GitProviderOwner>(),
Project = "ExampleProject".As<AzureDevOpsProjectName>(),
Handler = handler,
};

GitHostingRequestException exception = await Assert.ThrowsExactlyAsync<GitHostingRequestException>(
async () => await provider.GetPullRequestsAsync("example-repo".As<GitRepositoryName>(), TestContext.CancellationTokenSource.Token).ConfigureAwait(false))

Check warning on line 749 in GitIntegration.Test/Hosting/AzureDevOpsProviderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'TestContext.CancellationToken' instead of 'TestContext.CancellationTokenSource.Token'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaCauMXJkhlzGgA4wp5Y&open=AaCauMXJkhlzGgA4wp5Y&pullRequest=107
.ConfigureAwait(false);

Assert.AreEqual(HttpStatusCode.OK, exception.StatusCode);
StringAssert.Contains(exception.Message, "unrecognised pull request status");

Check warning on line 753 in GitIntegration.Test/Hosting/AzureDevOpsProviderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaCauMXJkhlzGgA4wp5W&open=AaCauMXJkhlzGgA4wp5W&pullRequest=107
StringAssert.Contains(exception.ResponseBody, "\"status\": \"notSet\"");

Check warning on line 754 in GitIntegration.Test/Hosting/AzureDevOpsProviderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaCauMXJkhlzGgA4wp5X&open=AaCauMXJkhlzGgA4wp5X&pullRequest=107
}

[TestMethod]
public async Task FetchesEveryPageOfPullRequestsAsync()
{
Expand Down
26 changes: 17 additions & 9 deletions GitIntegration/Hosting/AzureDevOpsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ internal override async Task<IReadOnlyList<GitPullRequest>> GetPullRequestsCoreA
body, AzureDevOpsJsonContext.Default.AzureDevOpsPullRequestListResponse, response.StatusCode);

IReadOnlyList<AzureDevOpsPullRequest> page = parsed?.Value ?? [];
pullRequests.AddRange(page.Select(ToGitPullRequest));
pullRequests.AddRange(page.Select(pullRequest => ToGitPullRequest(pullRequest, response.StatusCode, body)));

// Advanced by what actually arrived rather than by the page size asked for, so a
// service returning more than $top would skip past the entries it already sent
Expand All @@ -222,7 +222,7 @@ internal override async Task<IReadOnlyList<GitPullRequest>> GetPullRequestsCoreA
/// <see cref="GitPullRequestSpecification.Target"/> are bare branch names — this library's own
/// normalisation, matching what a caller gets back from every read path — so they are qualified
/// with <c>refs/heads/</c> here before being sent, the reverse of the stripping
/// <see cref="ToGitPullRequest(AzureDevOpsPullRequest)"/> does on the way back in. The response is
/// <see cref="ToGitPullRequest(AzureDevOpsPullRequest, HttpStatusCode, string)"/> does on the way back in. The response is
/// the created pull request; Microsoft's own worked example reports <c>201</c> despite the
/// endpoint's response table saying <c>200</c>, so this method checks
/// <see cref="HttpResponseMessage.IsSuccessStatusCode"/> rather than a specific status code.
Expand Down Expand Up @@ -271,7 +271,7 @@ internal override async Task<GitPullRequest> CreatePullRequestCoreAsync(string r
return parsed is null
? throw new GitHostingRequestException(
"Azure DevOps reported success but returned no pull request body.", Name, response.StatusCode, body)
: ToGitPullRequest(parsed);
: ToGitPullRequest(parsed, response.StatusCode, body);
}

/// <summary>
Expand Down Expand Up @@ -475,16 +475,18 @@ private static AuthenticationHeaderValue BasicAuthenticationHeader(string userna
/// composed from a constructed URL — see <see cref="AzureDevOpsReferenceLinks"/>'s remarks.
/// </remarks>
/// <param name="pullRequest">The pull request Azure DevOps returned.</param>
/// <param name="statusCode">The status code Azure DevOps reported for the response carrying <paramref name="pullRequest"/>.</param>
/// <param name="responseBody">The full response body carrying <paramref name="pullRequest"/>.</param>
/// <returns>The equivalent <see cref="GitPullRequest"/>.</returns>
private static GitPullRequest ToGitPullRequest(AzureDevOpsPullRequest pullRequest) => new()
private GitPullRequest ToGitPullRequest(AzureDevOpsPullRequest pullRequest, HttpStatusCode statusCode, string responseBody) => new()
{
Number = pullRequest.PullRequestId.ToString(CultureInfo.InvariantCulture).As<GitPullRequestNumber>(),
Title = (pullRequest.Title ?? string.Empty).As<GitPullRequestTitle>(),
Description = pullRequest.Description,
SourceBranch = StripRefsHeadsPrefix(pullRequest.SourceRefName ?? string.Empty).As<GitBranchName>(),
TargetBranch = StripRefsHeadsPrefix(pullRequest.TargetRefName ?? string.Empty).As<GitBranchName>(),
Author = pullRequest.CreatedBy?.UniqueName is string uniqueName ? uniqueName.As<GitPullRequestAuthor>() : null,
State = ToGitPullRequestState(pullRequest.Status),
State = ToGitPullRequestState(pullRequest.Status, statusCode, responseBody),
IsDraft = pullRequest.IsDraft,
WebURI = pullRequest.Links?.Web?.Href is string href ? href.As<GitPullRequestWebURI>() : null,
CreatedAt = pullRequest.CreationDate,
Expand All @@ -511,17 +513,23 @@ private static string StripRefsHeadsPrefix(string refName)
/// "Contradictions and gaps" entry 4): <c>active</c> → <see cref="GitPullRequestState.Open"/>,
/// <c>completed</c> → <see cref="GitPullRequestState.Merged"/>, <c>abandoned</c> →
/// <see cref="GitPullRequestState.Closed"/>. <c>notSet</c> and <c>all</c> are query-side-only
/// values a host never reports as a pull request's own status, so they fall through to the
/// unsupported case along with anything else unrecognised.
/// values a host never reports as a pull request's own status, so they fall through to a
/// <see cref="GitHostingRequestException"/> along with anything else unrecognised.
/// </remarks>
/// <param name="status">The status Azure DevOps reported.</param>
/// <param name="statusCode">The status code Azure DevOps reported for the response carrying <paramref name="status"/>.</param>
/// <param name="responseBody">The full response body carrying <paramref name="status"/>.</param>
/// <returns>The equivalent <see cref="GitPullRequestState"/>.</returns>
private static GitPullRequestState ToGitPullRequestState(string? status) => status switch
private GitPullRequestState ToGitPullRequestState(string? status, HttpStatusCode statusCode, string responseBody) => status switch
{
"active" => GitPullRequestState.Open,
"completed" => GitPullRequestState.Merged,
"abandoned" => GitPullRequestState.Closed,
_ => throw new NotSupportedException($"Azure DevOps reported an unrecognised pull request status '{status}'."),
_ => throw new GitHostingRequestException(
$"Azure DevOps reported an unrecognised pull request status '{status}'.",
Name,
statusCode,
responseBody),
};

/// <summary>
Expand Down
Loading