diff --git a/docs/design/sonar-mark/program.md b/docs/design/sonar-mark/program.md
index 98ea7a9..0e46f61 100644
--- a/docs/design/sonar-mark/program.md
+++ b/docs/design/sonar-mark/program.md
@@ -42,11 +42,8 @@ before attempting any network call. Missing required parameters are reported via
- `SonarMark-Cli-Help` — `Run` prints help and returns when `context.Help` is true
- `SonarMark-Server-Connect` — `ProcessSonarAnalysis` validates the `--server` parameter
- `SonarMark-Server-Auth` — `SonarQubeClient` is created with the token from context
-- `SonarMark-Server-QualityGate` — `GetQualityResultByBranchAsync` fetches quality gate status
-- `SonarMark-Server-Issues` — quality result includes the issues collection
-- `SonarMark-Server-HotSpots` — quality result includes the hot-spots collection
- `SonarMark-Server-ProjectKey` — `ProcessSonarAnalysis` validates the `--project-key` parameter
- `SonarMark-Server-Branch` — branch is passed through to `GetQualityResultByBranchAsync`
- `SonarMark-Report-Markdown` — `File.WriteAllText` writes the markdown report when `--report` is set
-- `SonarMark-Validation-Run` — `Run` delegates to `Validation.Run` when `context.Validate` is true
+- `SonarMark-Report-Depth` — `context.ReportDepth` is passed to `ToMarkdown` and shown in help text
- `SonarMark-Enforce-ExitCode` — `context.ExitCode` is returned from `Main`
diff --git a/docs/design/sonar-mark/report-generation/sonar-quality-result.md b/docs/design/sonar-mark/report-generation/sonar-quality-result.md
index 098a7d5..b70ca98 100644
--- a/docs/design/sonar-mark/report-generation/sonar-quality-result.md
+++ b/docs/design/sonar-mark/report-generation/sonar-quality-result.md
@@ -37,9 +37,6 @@ output and parsed by tools that understand that format.
## Satisfies Requirements
-- `SonarMark-Server-QualityGate` — `QualityGateStatus` and `Conditions` hold the quality gate data
-- `SonarMark-Server-Issues` — `Issues` holds the list of fetched issues
-- `SonarMark-Server-HotSpots` — `HotSpots` holds the list of fetched hot-spots
- `SonarMark-Report-Markdown` — `ToMarkdown` generates the markdown report content
- `SonarMark-Report-Depth` — the `depth` parameter controls heading levels
- `SonarMark-Report-QualityGate` — quality gate status and conditions are included in the report
diff --git a/docs/design/sonar-mark/sonar-integration/sonar-qube-client.md b/docs/design/sonar-mark/sonar-integration/sonar-qube-client.md
index fd5e56a..58fdbe7 100644
--- a/docs/design/sonar-mark/sonar-integration/sonar-qube-client.md
+++ b/docs/design/sonar-mark/sonar-integration/sonar-qube-client.md
@@ -42,10 +42,6 @@ errors from network errors and report them appropriately.
## Satisfies Requirements
-- `SonarMark-Server-Connect` — establishes HTTP connections to the SonarQube/SonarCloud server
-- `SonarMark-Server-Auth` — applies token-based Basic Authentication to all requests
- `SonarMark-Server-QualityGate` — fetches quality gate status and conditions from the API
- `SonarMark-Server-Issues` — fetches issues with pagination from the API
- `SonarMark-Server-HotSpots` — fetches security hot-spots with pagination from the API
-- `SonarMark-Server-ProjectKey` — passes the project key as a query parameter on all requests
-- `SonarMark-Server-Branch` — passes the branch name as an optional query parameter
diff --git a/src/DemaConsulting.SonarMark/SelfTest/Validation.cs b/src/DemaConsulting.SonarMark/SelfTest/Validation.cs
index 43ce98e..636f38d 100644
--- a/src/DemaConsulting.SonarMark/SelfTest/Validation.cs
+++ b/src/DemaConsulting.SonarMark/SelfTest/Validation.cs
@@ -33,6 +33,11 @@ namespace DemaConsulting.SonarMark.SelfTest;
///
internal static class Validation
{
+ ///
+ /// MIME type for JSON content used in mock HTTP responses.
+ ///
+ private const string JsonContentType = "application/json";
+
///
/// Special project key used for mock validation data.
///
@@ -64,7 +69,7 @@ public static void Run(Context context)
};
// Create mock HTTP client factory
- var mockFactory = (string? _) => new SonarQubeClient(CreateMockHttpClient(), false);
+ var mockFactory = (string? _) => new SonarQubeClient(CreateMockHttpClient(), true);
// Run core functionality tests
RunQualityGateRetrievalTest(context, testResults, mockFactory);
@@ -461,7 +466,7 @@ protected override Task SendAsync(HttpRequestMessage reques
""";
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
- Content = new StringContent(json, Encoding.UTF8, "application/json")
+ Content = new StringContent(json, Encoding.UTF8, JsonContentType)
});
}
@@ -493,7 +498,7 @@ protected override Task SendAsync(HttpRequestMessage reques
""";
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
- Content = new StringContent(json, Encoding.UTF8, "application/json")
+ Content = new StringContent(json, Encoding.UTF8, JsonContentType)
});
}
@@ -526,7 +531,7 @@ protected override Task SendAsync(HttpRequestMessage reques
""";
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
- Content = new StringContent(json, Encoding.UTF8, "application/json")
+ Content = new StringContent(json, Encoding.UTF8, JsonContentType)
});
}
@@ -549,7 +554,7 @@ protected override Task SendAsync(HttpRequestMessage reques
""";
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
- Content = new StringContent(json, Encoding.UTF8, "application/json")
+ Content = new StringContent(json, Encoding.UTF8, JsonContentType)
});
}
@@ -572,7 +577,7 @@ protected override Task SendAsync(HttpRequestMessage reques
""";
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
- Content = new StringContent(json, Encoding.UTF8, "application/json")
+ Content = new StringContent(json, Encoding.UTF8, JsonContentType)
});
}
@@ -620,7 +625,7 @@ public void Dispose()
Directory.Delete(DirectoryPath, true);
}
}
- catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or DirectoryNotFoundException)
{
// Ignore cleanup errors during disposal
}
diff --git a/src/DemaConsulting.SonarMark/SonarIntegration/SonarQubeClient.cs b/src/DemaConsulting.SonarMark/SonarIntegration/SonarQubeClient.cs
index c812aed..17aacd5 100644
--- a/src/DemaConsulting.SonarMark/SonarIntegration/SonarQubeClient.cs
+++ b/src/DemaConsulting.SonarMark/SonarIntegration/SonarQubeClient.cs
@@ -129,7 +129,7 @@ private async Task GetProjectNameByKeyAsync(
CancellationToken cancellationToken)
{
// Build API URL for component information
- var url = $"{serverUrl.TrimEnd('/')}/api/components/show?component={projectKey}";
+ var url = $"{serverUrl.TrimEnd('/')}/api/components/show?component={Uri.EscapeDataString(projectKey)}";
// Fetch component data from server
using var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
@@ -177,7 +177,7 @@ private async Task GetProjectNameByKeyAsync(
CancellationToken cancellationToken)
{
// Build API URL with project key and optional branch parameter
- var url = $"{serverUrl.TrimEnd('/')}/api/qualitygates/project_status?projectKey={projectKey}";
+ var url = $"{serverUrl.TrimEnd('/')}/api/qualitygates/project_status?projectKey={Uri.EscapeDataString(projectKey)}";
if (!string.IsNullOrWhiteSpace(branch))
{
url += $"&branch={Uri.EscapeDataString(branch)}";
@@ -272,7 +272,7 @@ private async Task> GetIssuesAsync(
{
// Build base API URL with project key and issue statuses filter
var baseUrl =
- $"{serverUrl.TrimEnd('/')}/api/issues/search?componentKeys={projectKey}&issueStatuses=OPEN,CONFIRMED&ps=100";
+ $"{serverUrl.TrimEnd('/')}/api/issues/search?componentKeys={Uri.EscapeDataString(projectKey)}&issueStatuses=OPEN,CONFIRMED&ps=100";
if (!string.IsNullOrWhiteSpace(branch))
{
baseUrl += $"&branch={Uri.EscapeDataString(branch)}";
@@ -339,7 +339,7 @@ private async Task> GetHotSpotsAsync(
CancellationToken cancellationToken)
{
// Build base API URL with project key and page size
- var baseUrl = $"{serverUrl.TrimEnd('/')}/api/hotspots/search?projectKey={projectKey}&ps=100";
+ var baseUrl = $"{serverUrl.TrimEnd('/')}/api/hotspots/search?projectKey={Uri.EscapeDataString(projectKey)}&ps=100";
if (!string.IsNullOrWhiteSpace(branch))
{
baseUrl += $"&branch={Uri.EscapeDataString(branch)}";
@@ -401,31 +401,38 @@ private async Task> FetchPaginatedAsync(
break;
}
- var pageIndex = pagingElement.TryGetProperty("pageIndex", out var pageIndexElement)
- ? pageIndexElement.GetInt32()
- : pageNumber;
- var pageSize = pagingElement.TryGetProperty("pageSize", out var pageSizeElement)
- ? pageSizeElement.GetInt32()
- : 100;
- var total = pagingElement.TryGetProperty("total", out var totalElement)
- ? totalElement.GetInt32()
- : 0;
-
- // Stop when all pages have been retrieved
- if (total > pageIndex * pageSize)
- {
- pageNumber++;
- }
- else
+ if (!HasMorePages(pagingElement, pageNumber))
{
break;
}
+
+ pageNumber++;
}
while (true);
return allItems;
}
+ ///
+ /// Determines whether additional pages remain based on the paging element of a SonarQube API response.
+ ///
+ /// The paging JSON element from the API response.
+ /// The current page number (used as fallback when pageIndex is absent).
+ /// True if more pages remain; otherwise false.
+ private static bool HasMorePages(JsonElement pagingElement, int pageNumber)
+ {
+ var pageIndex = pagingElement.TryGetProperty("pageIndex", out var pageIndexElement)
+ ? pageIndexElement.GetInt32()
+ : pageNumber;
+ var pageSize = pagingElement.TryGetProperty("pageSize", out var pageSizeElement)
+ ? pageSizeElement.GetInt32()
+ : 100;
+ var total = pagingElement.TryGetProperty("total", out var totalElement)
+ ? totalElement.GetInt32()
+ : 0;
+ return total > pageIndex * pageSize;
+ }
+
///
/// Parses hot-spots from a JSON array element
///