save progress
This commit is contained in:
@@ -0,0 +1,471 @@
|
||||
// <copyright file="GitHubCodeScanningClientTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
using StellaOps.Integrations.Plugin.GitHubApp.CodeScanning;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Integrations.Tests.CodeScanning;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="GitHubCodeScanningClient"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class GitHubCodeScanningClientTests
|
||||
{
|
||||
private readonly Mock<HttpMessageHandler> _httpHandlerMock;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public GitHubCodeScanningClientTests()
|
||||
{
|
||||
_httpHandlerMock = new Mock<HttpMessageHandler>();
|
||||
|
||||
var httpClient = new HttpClient(_httpHandlerMock.Object)
|
||||
{
|
||||
BaseAddress = new Uri("https://api.github.com")
|
||||
};
|
||||
|
||||
var factoryMock = new Mock<IHttpClientFactory>();
|
||||
factoryMock
|
||||
.Setup(f => f.CreateClient(GitHubCodeScanningClient.HttpClientName))
|
||||
.Returns(httpClient);
|
||||
|
||||
_httpClientFactory = factoryMock.Object;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadSarifAsync_Success_ReturnsResult()
|
||||
{
|
||||
// Arrange
|
||||
var responseJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
id = "sarif-123",
|
||||
url = "https://api.github.com/repos/owner/repo/code-scanning/sarifs/sarif-123"
|
||||
});
|
||||
|
||||
SetupHttpResponse(HttpStatusCode.Accepted, responseJson);
|
||||
|
||||
var client = CreateClient();
|
||||
var request = new SarifUploadRequest
|
||||
{
|
||||
CommitSha = "a".PadRight(40, 'b'),
|
||||
Ref = "refs/heads/main",
|
||||
SarifContent = "{\"version\":\"2.1.0\",\"runs\":[]}"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await client.UploadSarifAsync("owner", "repo", request, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Id.Should().Be("sarif-123");
|
||||
result.Status.Should().Be(ProcessingStatus.Pending);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadSarifAsync_InvalidCommitSha_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var client = CreateClient();
|
||||
var request = new SarifUploadRequest
|
||||
{
|
||||
CommitSha = "short",
|
||||
Ref = "refs/heads/main",
|
||||
SarifContent = "{}"
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentException>(
|
||||
() => client.UploadSarifAsync("owner", "repo", request, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadSarifAsync_InvalidRef_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var client = CreateClient();
|
||||
var request = new SarifUploadRequest
|
||||
{
|
||||
CommitSha = "a".PadRight(40, 'b'),
|
||||
Ref = "main", // Missing refs/ prefix
|
||||
SarifContent = "{}"
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentException>(
|
||||
() => client.UploadSarifAsync("owner", "repo", request, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUploadStatusAsync_Complete_ReturnsStatus()
|
||||
{
|
||||
// Arrange
|
||||
var responseJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
processing_status = "complete",
|
||||
analyses_url = "https://api.github.com/repos/owner/repo/code-scanning/analyses",
|
||||
results_count = 5,
|
||||
rules_count = 3
|
||||
});
|
||||
|
||||
SetupHttpResponse(HttpStatusCode.OK, responseJson);
|
||||
|
||||
var client = CreateClient();
|
||||
|
||||
// Act
|
||||
var status = await client.GetUploadStatusAsync("owner", "repo", "sarif-123", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
status.Status.Should().Be(ProcessingStatus.Complete);
|
||||
status.ResultsCount.Should().Be(5);
|
||||
status.RulesCount.Should().Be(3);
|
||||
status.IsComplete.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUploadStatusAsync_Pending_ReturnsStatus()
|
||||
{
|
||||
// Arrange
|
||||
var responseJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
processing_status = "pending"
|
||||
});
|
||||
|
||||
SetupHttpResponse(HttpStatusCode.OK, responseJson);
|
||||
|
||||
var client = CreateClient();
|
||||
|
||||
// Act
|
||||
var status = await client.GetUploadStatusAsync("owner", "repo", "sarif-123", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
status.Status.Should().Be(ProcessingStatus.Pending);
|
||||
status.IsInProgress.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUploadStatusAsync_Failed_ReturnsErrors()
|
||||
{
|
||||
// Arrange
|
||||
var responseJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
processing_status = "failed",
|
||||
errors = new[] { "Invalid SARIF", "Missing runs" }
|
||||
});
|
||||
|
||||
SetupHttpResponse(HttpStatusCode.OK, responseJson);
|
||||
|
||||
var client = CreateClient();
|
||||
|
||||
// Act
|
||||
var status = await client.GetUploadStatusAsync("owner", "repo", "sarif-123", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
status.Status.Should().Be(ProcessingStatus.Failed);
|
||||
status.Errors.Should().HaveCount(2);
|
||||
status.Errors.Should().Contain("Invalid SARIF");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListAlertsAsync_ReturnsAlerts()
|
||||
{
|
||||
// Arrange
|
||||
var alertsData = new object[]
|
||||
{
|
||||
new
|
||||
{
|
||||
number = 1,
|
||||
state = "open",
|
||||
rule = new { id = "csharp/sql-injection", severity = "high", description = "SQL injection" },
|
||||
tool = new { name = "StellaOps", version = "1.0" },
|
||||
html_url = "https://github.com/owner/repo/security/code-scanning/1",
|
||||
created_at = "2026-01-09T10:00:00Z"
|
||||
},
|
||||
new
|
||||
{
|
||||
number = 2,
|
||||
state = "dismissed",
|
||||
rule = new { id = "csharp/xss", severity = "medium", description = "XSS vulnerability" },
|
||||
tool = new { name = "StellaOps", version = "1.0" },
|
||||
html_url = "https://github.com/owner/repo/security/code-scanning/2",
|
||||
created_at = "2026-01-08T10:00:00Z",
|
||||
dismissed_at = "2026-01-09T11:00:00Z",
|
||||
dismissed_reason = "false_positive"
|
||||
}
|
||||
};
|
||||
var responseJson = JsonSerializer.Serialize(alertsData);
|
||||
|
||||
SetupHttpResponse(HttpStatusCode.OK, responseJson);
|
||||
|
||||
var client = CreateClient();
|
||||
|
||||
// Act
|
||||
var alerts = await client.ListAlertsAsync("owner", "repo", null, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
alerts.Should().HaveCount(2);
|
||||
alerts[0].Number.Should().Be(1);
|
||||
alerts[0].State.Should().Be("open");
|
||||
alerts[0].RuleId.Should().Be("csharp/sql-injection");
|
||||
alerts[1].DismissedReason.Should().Be("false_positive");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListAlertsAsync_WithFilter_AppliesQueryString()
|
||||
{
|
||||
// Arrange
|
||||
SetupHttpResponse(HttpStatusCode.OK, "[]");
|
||||
|
||||
var client = CreateClient();
|
||||
var filter = new AlertFilter
|
||||
{
|
||||
State = "open",
|
||||
Severity = "high",
|
||||
PerPage = 50
|
||||
};
|
||||
|
||||
// Act
|
||||
await client.ListAlertsAsync("owner", "repo", filter, CancellationToken.None);
|
||||
|
||||
// Assert - Verify the request URL contained query parameters
|
||||
_httpHandlerMock.Protected().Verify(
|
||||
"SendAsync",
|
||||
Times.Once(),
|
||||
ItExpr.Is<HttpRequestMessage>(req =>
|
||||
req.RequestUri!.Query.Contains("state=open") &&
|
||||
req.RequestUri.Query.Contains("severity=high") &&
|
||||
req.RequestUri.Query.Contains("per_page=50")),
|
||||
ItExpr.IsAny<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAlertAsync_ReturnsAlert()
|
||||
{
|
||||
// Arrange
|
||||
var responseJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
number = 42,
|
||||
state = "open",
|
||||
rule = new { id = "csharp/path-traversal", severity = "critical", description = "Path traversal" },
|
||||
tool = new { name = "StellaOps" },
|
||||
html_url = "https://github.com/owner/repo/security/code-scanning/42",
|
||||
created_at = "2026-01-09T10:00:00Z",
|
||||
most_recent_instance = new
|
||||
{
|
||||
@ref = "refs/heads/main",
|
||||
location = new
|
||||
{
|
||||
path = "src/Controllers/FileController.cs",
|
||||
start_line = 42,
|
||||
end_line = 45
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
SetupHttpResponse(HttpStatusCode.OK, responseJson);
|
||||
|
||||
var client = CreateClient();
|
||||
|
||||
// Act
|
||||
var alert = await client.GetAlertAsync("owner", "repo", 42, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
alert.Number.Should().Be(42);
|
||||
alert.RuleSeverity.Should().Be("critical");
|
||||
alert.MostRecentInstance.Should().NotBeNull();
|
||||
alert.MostRecentInstance!.Location!.Path.Should().Be("src/Controllers/FileController.cs");
|
||||
alert.MostRecentInstance.Location.StartLine.Should().Be(42);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAlertAsync_Dismiss_ReturnsUpdatedAlert()
|
||||
{
|
||||
// Arrange
|
||||
var responseJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
number = 1,
|
||||
state = "dismissed",
|
||||
rule = new { id = "test", severity = "low", description = "Test" },
|
||||
tool = new { name = "StellaOps" },
|
||||
html_url = "https://github.com/owner/repo/security/code-scanning/1",
|
||||
created_at = "2026-01-09T10:00:00Z",
|
||||
dismissed_at = "2026-01-09T12:00:00Z",
|
||||
dismissed_reason = "false_positive"
|
||||
});
|
||||
|
||||
SetupHttpResponse(HttpStatusCode.OK, responseJson);
|
||||
|
||||
var client = CreateClient();
|
||||
var update = new AlertUpdate
|
||||
{
|
||||
State = "dismissed",
|
||||
DismissedReason = "false_positive",
|
||||
DismissedComment = "Not applicable to our use case"
|
||||
};
|
||||
|
||||
// Act
|
||||
var alert = await client.UpdateAlertAsync("owner", "repo", 1, update, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
alert.State.Should().Be("dismissed");
|
||||
alert.DismissedReason.Should().Be("false_positive");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAlertAsync_InvalidState_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var client = CreateClient();
|
||||
var update = new AlertUpdate
|
||||
{
|
||||
State = "invalid"
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentException>(
|
||||
() => client.UpdateAlertAsync("owner", "repo", 1, update, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAlertAsync_DismissWithoutReason_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var client = CreateClient();
|
||||
var update = new AlertUpdate
|
||||
{
|
||||
State = "dismissed"
|
||||
// Missing DismissedReason
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentException>(
|
||||
() => client.UpdateAlertAsync("owner", "repo", 1, update, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadSarifAsync_Unauthorized_ThrowsGitHubApiException()
|
||||
{
|
||||
// Arrange
|
||||
SetupHttpResponse(HttpStatusCode.Unauthorized, "{\"message\":\"Bad credentials\"}");
|
||||
|
||||
var client = CreateClient();
|
||||
var request = new SarifUploadRequest
|
||||
{
|
||||
CommitSha = "a".PadRight(40, 'b'),
|
||||
Ref = "refs/heads/main",
|
||||
SarifContent = "{}"
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Assert.ThrowsAsync<GitHubApiException>(
|
||||
() => client.UploadSarifAsync("owner", "repo", request, CancellationToken.None));
|
||||
|
||||
ex.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
ex.Message.Should().Contain("authentication");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadSarifAsync_NotFound_ThrowsGitHubApiException()
|
||||
{
|
||||
// Arrange
|
||||
SetupHttpResponse(HttpStatusCode.NotFound, "{\"message\":\"Not Found\"}");
|
||||
|
||||
var client = CreateClient();
|
||||
var request = new SarifUploadRequest
|
||||
{
|
||||
CommitSha = "a".PadRight(40, 'b'),
|
||||
Ref = "refs/heads/main",
|
||||
SarifContent = "{}"
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Assert.ThrowsAsync<GitHubApiException>(
|
||||
() => client.UploadSarifAsync("owner", "repo", request, CancellationToken.None));
|
||||
|
||||
ex.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlertFilter_ToQueryString_BuildsCorrectQuery()
|
||||
{
|
||||
// Arrange
|
||||
var filter = new AlertFilter
|
||||
{
|
||||
State = "open",
|
||||
Severity = "high",
|
||||
Tool = "StellaOps",
|
||||
Ref = "refs/heads/main",
|
||||
PerPage = 100,
|
||||
Page = 2,
|
||||
Sort = "created",
|
||||
Direction = "desc"
|
||||
};
|
||||
|
||||
// Act
|
||||
var query = filter.ToQueryString();
|
||||
|
||||
// Assert
|
||||
query.Should().Contain("state=open");
|
||||
query.Should().Contain("severity=high");
|
||||
query.Should().Contain("tool_name=StellaOps");
|
||||
query.Should().Contain("per_page=100");
|
||||
query.Should().Contain("page=2");
|
||||
query.Should().Contain("sort=created");
|
||||
query.Should().Contain("direction=desc");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlertFilter_ToQueryString_Empty_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var filter = new AlertFilter();
|
||||
|
||||
// Act
|
||||
var query = filter.ToQueryString();
|
||||
|
||||
// Assert
|
||||
query.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SarifUploadRequest_Validate_EmptySarif_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var request = new SarifUploadRequest
|
||||
{
|
||||
CommitSha = "a".PadRight(40, 'b'),
|
||||
Ref = "refs/heads/main",
|
||||
SarifContent = ""
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentException>(() => request.Validate());
|
||||
}
|
||||
|
||||
private GitHubCodeScanningClient CreateClient()
|
||||
{
|
||||
return new GitHubCodeScanningClient(
|
||||
_httpClientFactory,
|
||||
NullLogger<GitHubCodeScanningClient>.Instance,
|
||||
TimeProvider.System);
|
||||
}
|
||||
|
||||
private void SetupHttpResponse(HttpStatusCode statusCode, string content)
|
||||
{
|
||||
_httpHandlerMock.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>(
|
||||
"SendAsync",
|
||||
ItExpr.IsAny<HttpRequestMessage>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(new HttpResponseMessage
|
||||
{
|
||||
StatusCode = statusCode,
|
||||
Content = new StringContent(content)
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user