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
39 changes: 39 additions & 0 deletions GitBranchStateCache.Tests/Admission/AdmissionGateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,45 @@ public async Task AdmitAsync_WhenTheForgeIsUnreachable_StillRefuses()
Assert.AreEqual(502, outcome.StatusCode);
}

[TestMethod]
[DataRow("fatal: unable to access 'https://nonexistent-host.invalid/studio/game-401.git/': CONNECT tunnel failed, response 502")]
[DataRow("fatal: unable to access 'https://github.com/studio/game-403.git/': Could not resolve host: github.com")]
[DataRow("fatal: unable to access 'https://forge.example:4401/studio/game.git/': Failed to connect to forge.example port 4401")]
public async Task AdmitAsync_WhenTheForgeIsUnreachableAtAUrlContainingAStatusCode_Is502(string error)
{
// Git echoes the repository URL, so digits in it must not be read as the status the forge sent.
(AdmissionGate gate, FakeGitRunner runner, _) = Build();
runner.Respond = _ => new GitResult(128, string.Empty, error, TimedOut: false);

Assert.AreEqual(502, (await AdmitAsync(gate)).StatusCode);
}

[TestMethod]
[DataRow("fatal: unable to access 'https://github.com/studio/game.git/': The requested URL returned error: 401")]
[DataRow("remote: Permission to studio/game.git denied to someone.\nfatal: unable to access 'https://github.com/studio/game.git/': The requested URL returned error: 403")]
[DataRow("error: RPC failed; HTTP 403 curl 22 The requested URL returned error: 403")]
public async Task AdmitAsync_WhenGitReportsA401Or403_Is401(string error)
{
(AdmissionGate gate, FakeGitRunner runner, _) = Build();
runner.Respond = _ => new GitResult(128, string.Empty, error, TimedOut: false);

Assert.AreEqual(401, (await AdmitAsync(gate)).StatusCode);
}

[TestMethod]
public async Task AdmitAsync_WhenAzureDevOpsReportsTF401019_Is404()
{
// TF401019 is Azure DevOps's "repository does not exist", whatever its code looks like.
(AdmissionGate gate, FakeGitRunner runner, _) = Build();
runner.Respond = _ => new GitResult(
128,
string.Empty,
"remote: TF401019: The Git repository with name or identifier game does not exist or you do not have permissions for the operation you are attempting.",
TimedOut: false);

Assert.AreEqual(404, (await AdmitAsync(gate)).StatusCode);
}

[TestMethod]
public async Task AdmitAsync_WhenTheProbeTimesOut_Refuses()
{
Expand Down
12 changes: 10 additions & 2 deletions GitBranchStateCache/Admission/AdmissionGate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ public sealed class AdmissionGate(
/// Matched to choose a status code and nothing else. Every branch of this refuses; the only
/// question is whether the caller is told to fix their credential or that the forge could not be
/// reached, and getting that wrong costs a confusing message rather than an incorrect decision.
/// <para>
/// A status code is only matched in git's own phrasing, never as bare digits. Git echoes the
/// repository URL in almost every failure, so a bare <c>"401"</c> would read a DNS or proxy failure
/// for <c>studio/game-401.git</c> as a refused credential, and Azure DevOps's
/// <c>TF401019</c> "repository does not exist" as one too.
/// </para>
/// </remarks>
private static readonly string[] AuthenticationMarkers =
[
Expand All @@ -48,8 +54,10 @@ public sealed class AdmissionGate(
"could not read password",
"invalid username or password",
"http basic: access denied",
"403",
"401",
"returned error: 401",
"returned error: 403",
"http 401",
"http 403",
];

/// <summary>
Expand Down
Loading