162 lines
7.0 KiB
C#
162 lines
7.0 KiB
C#
using System.Net;
|
|
using System.Text.Json.Nodes;
|
|
using ClaudeDo.Worker.AzureDevOps;
|
|
using ClaudeDo.Worker.Tests.Tickets;
|
|
|
|
namespace ClaudeDo.Worker.Tests.AzureDevOps;
|
|
|
|
/// <summary>Port of pr-create.ps1: its -SelfTest cases plus the PR find-or-create flow, HTTP stubbed.</summary>
|
|
public sealed class AzureDevOpsClientTests : IDisposable
|
|
{
|
|
private readonly string _dir = Path.Combine(Path.GetTempPath(), $"cd_azdo_{Guid.NewGuid():N}");
|
|
|
|
public void Dispose()
|
|
{
|
|
try { Directory.Delete(_dir, true); } catch { }
|
|
}
|
|
|
|
internal static AzureDevOpsPatStore PatStore(string dir, string? pat = "geheim")
|
|
{
|
|
var store = new AzureDevOpsPatStore(new DpapiTokenStore(Path.Combine(dir, "azdo.pat")));
|
|
if (pat is not null) store.Store.Save(pat);
|
|
return store;
|
|
}
|
|
|
|
private AzureDevOpsClient Client(StubHandler handler, string? pat = "geheim") =>
|
|
new(new StubHttpClientFactory(handler), PatStore(_dir, pat));
|
|
|
|
private static readonly AzdoRepo Repo = new("https://srv.local/Coll/Proj", "Repo", "https://srv.local/Coll/Proj/_git/Repo");
|
|
|
|
[Theory]
|
|
[InlineData("https://mika@srv.local/Coll/Proj/_git/Repo.git", "https://srv.local/Coll/Proj", "Repo")]
|
|
[InlineData("https://srv.local/tfs/Coll/Proj/_git/Mein%20Repo", "https://srv.local/tfs/Coll/Proj", "Mein Repo")]
|
|
public void ParseRemote_SelfTestCases(string remote, string apiBase, string repo)
|
|
{
|
|
var ctx = AzureDevOpsClient.ParseRemote(remote);
|
|
|
|
Assert.NotNull(ctx);
|
|
Assert.Equal(apiBase, ctx!.ApiBase);
|
|
Assert.Equal(repo, ctx.Repo);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseRemote_StripsCredentialsFromWebUrl()
|
|
{
|
|
var ctx = AzureDevOpsClient.ParseRemote("https://mika:pw@srv.local/Coll/Proj/_git/Repo.git");
|
|
Assert.Equal("https://srv.local/Coll/Proj/_git/Repo", ctx!.WebUrl);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("https://example.com/mika/repo.git")]
|
|
[InlineData("C:/repos/coll/_git/repo")]
|
|
[InlineData("")]
|
|
public void ParseRemote_NonAdoRemote_IsNull(string remote) => Assert.Null(AzureDevOpsClient.ParseRemote(remote));
|
|
|
|
[Fact]
|
|
public void AuthHeader_MatchesScript() => Assert.Equal("Basic OmdlaGVpbQ==", AzureDevOpsClient.AuthHeaderValue("geheim"));
|
|
|
|
[Fact]
|
|
public void GitAuthEnv_PassesPatAsExtraHeaderAndNeverPrompts()
|
|
{
|
|
var env = AzureDevOpsClient.GitAuthEnv("geheim");
|
|
|
|
Assert.Equal("0", env["GIT_TERMINAL_PROMPT"]);
|
|
Assert.Equal("never", env["GCM_INTERACTIVE"]);
|
|
Assert.Equal("1", env["GIT_CONFIG_COUNT"]);
|
|
Assert.Equal("http.extraHeader", env["GIT_CONFIG_KEY_0"]);
|
|
Assert.Equal("Authorization: Basic OmdlaGVpbQ==", env["GIT_CONFIG_VALUE_0"]);
|
|
Assert.False(AzureDevOpsClient.GitAuthEnv(null).ContainsKey("GIT_CONFIG_COUNT"));
|
|
}
|
|
|
|
[Fact]
|
|
public void SupportedVersion_ReadsOnlyTheOutOfRangeError()
|
|
{
|
|
Assert.Equal("6.0", AzureDevOpsClient.SupportedVersion(
|
|
"The requested REST API version of 7.1 is out of range for this server. The latest version this server supports is 6.0."));
|
|
Assert.Null(AzureDevOpsClient.SupportedVersion("TF401019: irgendein anderer Fehler"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EnsureDraftPullRequest_ReusesActivePr_WithoutPost()
|
|
{
|
|
var handler = new StubHandler(_ => (HttpStatusCode.OK, """{"value":[{"pullRequestId":42}],"count":1}"""));
|
|
|
|
var (url, created) = await Client(handler).EnsureDraftPullRequestAsync(Repo, "feature/1-x", "main", "t", "d", default);
|
|
|
|
Assert.False(created);
|
|
Assert.Equal("https://srv.local/Coll/Proj/_git/Repo/pullrequest/42", url);
|
|
Assert.Single(handler.Requests);
|
|
Assert.Contains("searchCriteria.sourceRefName=refs%2Fheads%2Ffeature%2F1-x", handler.Requests[0].RequestUri!.Query);
|
|
Assert.Contains("searchCriteria.status=active", handler.Requests[0].RequestUri!.Query);
|
|
Assert.Equal("Basic OmdlaGVpbQ==", handler.Requests[0].Headers.Authorization!.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EnsureDraftPullRequest_CreatesDraftWhenNoneActive()
|
|
{
|
|
var handler = new StubHandler(req => req.Method == HttpMethod.Get
|
|
? (HttpStatusCode.OK, """{"value":[],"count":0}""")
|
|
: (HttpStatusCode.Created, """{"pullRequestId":7}"""));
|
|
|
|
var (url, created) = await Client(handler).EnsureDraftPullRequestAsync(Repo, "feature/1-x", "dev", "Titel", "Beschreibung", default);
|
|
|
|
Assert.True(created);
|
|
Assert.EndsWith("/pullrequest/7", url);
|
|
var body = JsonNode.Parse(handler.Bodies[1])!;
|
|
Assert.True(body["isDraft"]!.GetValue<bool>());
|
|
Assert.Equal("refs/heads/feature/1-x", body["sourceRefName"]!.GetValue<string>());
|
|
Assert.Equal("refs/heads/dev", body["targetRefName"]!.GetValue<string>());
|
|
Assert.Equal("Titel", body["title"]!.GetValue<string>());
|
|
Assert.Equal("Beschreibung", body["description"]!.GetValue<string>());
|
|
Assert.Equal("https://srv.local/Coll/Proj/_apis/git/repositories/Repo/pullrequests", handler.Requests[1].RequestUri!.GetLeftPart(UriPartial.Path));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ApiVersion_FallsBackToTheVersionTheServerNames()
|
|
{
|
|
var handler = new StubHandler(req => req.RequestUri!.Query.Contains("api-version=6.0")
|
|
? (HttpStatusCode.BadRequest, """{"message":"The requested REST API version of 6.0 is out of range for this server. The latest version this server supports is 5.1."}""")
|
|
: (HttpStatusCode.OK, """{"defaultBranch":"refs/heads/dev"}"""));
|
|
|
|
var branch = await Client(handler).GetDefaultBranchAsync(Repo, default);
|
|
|
|
Assert.Equal("dev", branch);
|
|
Assert.Equal(2, handler.Requests.Count);
|
|
Assert.Contains("api-version=5.1", handler.Requests[1].RequestUri!.Query);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RestError_SurfacesStatusAndServerMessage()
|
|
{
|
|
var handler = new StubHandler(_ => (HttpStatusCode.Forbidden, """{"message":"TF401027: You need the Git 'PullRequestContribute' permission."}"""));
|
|
|
|
var ex = await Assert.ThrowsAsync<AzureDevOpsException>(() => Client(handler).GetDefaultBranchAsync(Repo, default));
|
|
|
|
Assert.Contains("403", ex.Message);
|
|
Assert.Contains("TF401027", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NoPat_FailsBeforeAnyRequest()
|
|
{
|
|
var handler = new StubHandler(_ => (HttpStatusCode.OK, "{}"));
|
|
|
|
await Assert.ThrowsAsync<AzureDevOpsException>(() => Client(handler, pat: null).GetDefaultBranchAsync(Repo, default));
|
|
Assert.Empty(handler.Requests);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RejectedPat_203LoginPage_IsAPatError()
|
|
{
|
|
var handler = new StubHandler(_ => ((HttpStatusCode)203, "<html>login</html>"));
|
|
|
|
var ex = await Assert.ThrowsAsync<AzureDevOpsException>(() => Client(handler).GetDefaultBranchAsync(Repo, default));
|
|
Assert.Contains("PAT", ex.Message);
|
|
}
|
|
}
|
|
|
|
internal sealed class StubHttpClientFactory(StubHandler handler) : IHttpClientFactory
|
|
{
|
|
public HttpClient CreateClient(string name) => new(handler, disposeHandler: false);
|
|
}
|