audit, advisories and doctors/setup work
This commit is contained in:
35
devops/AGENTS.md
Normal file
35
devops/AGENTS.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# AGENTS - DevOps
|
||||
|
||||
## Roles
|
||||
- DevOps engineer: maintain devops services, tools, and release assets.
|
||||
- QA engineer: add and maintain tests for devops services and tools.
|
||||
- Docs/PM: keep sprint status and devops docs aligned.
|
||||
|
||||
## Working directory
|
||||
- Primary: `devops/**`
|
||||
- Avoid edits outside devops unless a sprint explicitly allows it.
|
||||
|
||||
## Required reading (treat as read before DOING)
|
||||
- `docs/README.md`
|
||||
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
|
||||
- `docs/ARCHITECTURE_OVERVIEW.md`
|
||||
- `docs/operations/devops/architecture.md`
|
||||
- `docs/modules/platform/architecture-overview.md`
|
||||
- Sprint file under `docs/implplan/`.
|
||||
|
||||
## Coding standards
|
||||
- Target .NET 10; enable preview features when configured.
|
||||
- TreatWarningsAsErrors must be true in new projects.
|
||||
- Deterministic outputs only; avoid environment-dependent behavior.
|
||||
- Use invariant culture for parsing/formatting in production and tests.
|
||||
|
||||
## Testing
|
||||
- Use xUnit; tests must be offline-safe and deterministic.
|
||||
- For web services, prefer in-memory TestServer or WebApplicationFactory.
|
||||
|
||||
## Sprint/status discipline
|
||||
- Update sprint task status: TODO -> DOING -> DONE/BLOCKED.
|
||||
- Log execution updates and decisions in the sprint file.
|
||||
|
||||
## Contacts/ownership
|
||||
- Module owner: DevOps Guild
|
||||
12
devops/Directory.Packages.props
Normal file
12
devops/Directory.Packages.props
Normal file
@@ -0,0 +1,12 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="FluentAssertions" Version="8.8.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -126,3 +126,5 @@ public record KeysResponse(
|
||||
[property: JsonPropertyName("public_key_b64")] string PublicKeyBase64,
|
||||
[property: JsonPropertyName("curve")] string Curve,
|
||||
[property: JsonPropertyName("simulated_providers")] IEnumerable<string> Providers);
|
||||
|
||||
public partial class Program { }
|
||||
|
||||
@@ -7,4 +7,7 @@
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<AssetTargetFallback></AssetTargetFallback>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="__Tests/**" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
global using Xunit;
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SimCryptoService.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
|
||||
namespace SimCryptoService.Tests;
|
||||
|
||||
public sealed class SimCryptoServiceTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
|
||||
public SimCryptoServiceTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SignThenVerify_ReturnsOk()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
var signResponse = await client.PostAsJsonAsync("/sign", new SignRequest("hello", "SM2"));
|
||||
signResponse.IsSuccessStatusCode.Should().BeTrue();
|
||||
|
||||
var signPayload = await signResponse.Content.ReadFromJsonAsync<SignResponse>();
|
||||
signPayload.Should().NotBeNull();
|
||||
signPayload!.SignatureBase64.Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
var verifyResponse = await client.PostAsJsonAsync("/verify", new VerifyRequest("hello", signPayload.SignatureBase64, "SM2"));
|
||||
verifyResponse.IsSuccessStatusCode.Should().BeTrue();
|
||||
|
||||
var verifyPayload = await verifyResponse.Content.ReadFromJsonAsync<VerifyResponse>();
|
||||
verifyPayload.Should().NotBeNull();
|
||||
verifyPayload!.Ok.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Keys_ReturnsAlgorithmsAndKey()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
var response = await client.GetFromJsonAsync<KeysResponse>("/keys");
|
||||
response.Should().NotBeNull();
|
||||
response!.PublicKeyBase64.Should().NotBeNullOrWhiteSpace();
|
||||
response.SimulatedProviders.Should().Contain("SM2");
|
||||
response.SimulatedProviders.Should().Contain("GOST12-256");
|
||||
}
|
||||
|
||||
private sealed record SignRequest(
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
private sealed record SignResponse(
|
||||
[property: JsonPropertyName("signature_b64")] string SignatureBase64,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
private sealed record VerifyRequest(
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("signature_b64")] string SignatureBase64,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
private sealed record VerifyResponse(
|
||||
[property: JsonPropertyName("ok")] bool Ok,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
private sealed record KeysResponse(
|
||||
[property: JsonPropertyName("public_key_b64")] string PublicKeyBase64,
|
||||
[property: JsonPropertyName("curve")] string Curve,
|
||||
[property: JsonPropertyName("simulated_providers")] string[] SimulatedProviders);
|
||||
}
|
||||
@@ -1,61 +1,16 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
var baseUrl = Environment.GetEnvironmentVariable("STELLAOPS_CRYPTO_SIM_URL") ?? "http://localhost:8080";
|
||||
var profile = (Environment.GetEnvironmentVariable("SIM_PROFILE") ?? "sm").ToLowerInvariant();
|
||||
var algList = Environment.GetEnvironmentVariable("SIM_ALGORITHMS")?
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
?? profile switch
|
||||
{
|
||||
"ru-free" or "ru-paid" or "gost" or "ru" => new[] { "GOST12-256", "ru.magma.sim", "ru.kuznyechik.sim" },
|
||||
"sm" or "cn" => new[] { "SM2" },
|
||||
"eidas" => new[] { "ES256" },
|
||||
"fips" => new[] { "ES256" },
|
||||
"kcmvp" => new[] { "ES256" },
|
||||
"pq" => new[] { "pq.sim", "DILITHIUM3", "FALCON512" },
|
||||
_ => new[] { "ES256", "SM2", "pq.sim" }
|
||||
};
|
||||
var algList = SmokeLogic.ResolveAlgorithms(profile, Environment.GetEnvironmentVariable("SIM_ALGORITHMS"));
|
||||
var message = Environment.GetEnvironmentVariable("SIM_MESSAGE") ?? "stellaops-sim-smoke";
|
||||
|
||||
using var client = new HttpClient { BaseAddress = new Uri(baseUrl) };
|
||||
|
||||
static async Task<(bool Ok, string Error)> SignAndVerify(HttpClient client, string algorithm, string message, CancellationToken ct)
|
||||
{
|
||||
var signPayload = new SignRequest(message, algorithm);
|
||||
var signResponse = await client.PostAsJsonAsync("/sign", signPayload, ct).ConfigureAwait(false);
|
||||
if (!signResponse.IsSuccessStatusCode)
|
||||
{
|
||||
return (false, $"sign failed: {(int)signResponse.StatusCode} {signResponse.ReasonPhrase}");
|
||||
}
|
||||
|
||||
var signResult = await signResponse.Content.ReadFromJsonAsync<SignResponse>(cancellationToken: ct).ConfigureAwait(false);
|
||||
if (signResult is null || string.IsNullOrWhiteSpace(signResult.SignatureBase64))
|
||||
{
|
||||
return (false, "sign returned empty payload");
|
||||
}
|
||||
|
||||
var verifyPayload = new VerifyRequest(message, signResult.SignatureBase64, algorithm);
|
||||
var verifyResponse = await client.PostAsJsonAsync("/verify", verifyPayload, ct).ConfigureAwait(false);
|
||||
if (!verifyResponse.IsSuccessStatusCode)
|
||||
{
|
||||
return (false, $"verify failed: {(int)verifyResponse.StatusCode} {verifyResponse.ReasonPhrase}");
|
||||
}
|
||||
|
||||
var verifyResult = await verifyResponse.Content.ReadFromJsonAsync<VerifyResponse>(cancellationToken: ct).ConfigureAwait(false);
|
||||
if (verifyResult?.Ok is not true)
|
||||
{
|
||||
return (false, "verify returned false");
|
||||
}
|
||||
|
||||
return (true, "");
|
||||
}
|
||||
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
|
||||
var failures = new List<string>();
|
||||
|
||||
foreach (var alg in algList)
|
||||
{
|
||||
var (ok, error) = await SignAndVerify(client, alg, message, cts.Token);
|
||||
var (ok, error) = await SmokeLogic.SignAndVerifyAsync(client, alg, message, cts.Token);
|
||||
if (!ok)
|
||||
{
|
||||
failures.Add($"{alg}: {error}");
|
||||
@@ -77,20 +32,3 @@ if (failures.Count > 0)
|
||||
}
|
||||
|
||||
Console.WriteLine("Simulation smoke passed.");
|
||||
|
||||
internal sealed record SignRequest(
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
internal sealed record SignResponse(
|
||||
[property: JsonPropertyName("signature_b64")] string SignatureBase64,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
internal sealed record VerifyRequest(
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("signature_b64")] string SignatureBase64,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
internal sealed record VerifyResponse(
|
||||
[property: JsonPropertyName("ok")] bool Ok,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
@@ -8,4 +8,7 @@
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<AssetTargetFallback></AssetTargetFallback>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="__Tests/**" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
72
devops/services/crypto/sim-crypto-smoke/SmokeLogic.cs
Normal file
72
devops/services/crypto/sim-crypto-smoke/SmokeLogic.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
public static class SmokeLogic
|
||||
{
|
||||
public static IReadOnlyList<string> ResolveAlgorithms(string profile, string? overrideList)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(overrideList))
|
||||
{
|
||||
return overrideList.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
}
|
||||
|
||||
return profile switch
|
||||
{
|
||||
"ru-free" or "ru-paid" or "gost" or "ru" => new[] { "GOST12-256", "ru.magma.sim", "ru.kuznyechik.sim" },
|
||||
"sm" or "cn" => new[] { "SM2" },
|
||||
"eidas" => new[] { "ES256" },
|
||||
"fips" => new[] { "ES256" },
|
||||
"kcmvp" => new[] { "ES256" },
|
||||
"pq" => new[] { "pq.sim", "DILITHIUM3", "FALCON512" },
|
||||
_ => new[] { "ES256", "SM2", "pq.sim" }
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<(bool Ok, string Error)> SignAndVerifyAsync(HttpClient client, string algorithm, string message, CancellationToken ct)
|
||||
{
|
||||
var signPayload = new SignRequest(message, algorithm);
|
||||
var signResponse = await client.PostAsJsonAsync("/sign", signPayload, ct).ConfigureAwait(false);
|
||||
if (!signResponse.IsSuccessStatusCode)
|
||||
{
|
||||
return (false, $"sign failed: {(int)signResponse.StatusCode} {signResponse.ReasonPhrase}");
|
||||
}
|
||||
|
||||
var signResult = await signResponse.Content.ReadFromJsonAsync<SignResponse>(cancellationToken: ct).ConfigureAwait(false);
|
||||
if (signResult is null || string.IsNullOrWhiteSpace(signResult.SignatureBase64))
|
||||
{
|
||||
return (false, "sign returned empty payload");
|
||||
}
|
||||
|
||||
var verifyPayload = new VerifyRequest(message, signResult.SignatureBase64, algorithm);
|
||||
var verifyResponse = await client.PostAsJsonAsync("/verify", verifyPayload, ct).ConfigureAwait(false);
|
||||
if (!verifyResponse.IsSuccessStatusCode)
|
||||
{
|
||||
return (false, $"verify failed: {(int)verifyResponse.StatusCode} {verifyResponse.ReasonPhrase}");
|
||||
}
|
||||
|
||||
var verifyResult = await verifyResponse.Content.ReadFromJsonAsync<VerifyResponse>(cancellationToken: ct).ConfigureAwait(false);
|
||||
if (verifyResult?.Ok is not true)
|
||||
{
|
||||
return (false, "verify returned false");
|
||||
}
|
||||
|
||||
return (true, "");
|
||||
}
|
||||
|
||||
private sealed record SignRequest(
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
private sealed record SignResponse(
|
||||
[property: JsonPropertyName("signature_b64")] string SignatureBase64,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
private sealed record VerifyRequest(
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("signature_b64")] string SignatureBase64,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
private sealed record VerifyResponse(
|
||||
[property: JsonPropertyName("ok")] bool Ok,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
global using Xunit;
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SimCryptoSmoke.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace SimCryptoSmoke.Tests;
|
||||
|
||||
public sealed class SimCryptoSmokeTests
|
||||
{
|
||||
[Fact]
|
||||
public void ResolveAlgorithms_UsesProfileDefaults()
|
||||
{
|
||||
var algs = SmokeLogic.ResolveAlgorithms("gost", null);
|
||||
algs.Should().Contain("GOST12-256");
|
||||
algs.Should().Contain("ru.magma.sim");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveAlgorithms_UsesOverrideList()
|
||||
{
|
||||
var algs = SmokeLogic.ResolveAlgorithms("sm", "ES256,SM2");
|
||||
algs.Should().ContainInOrder(new[] { "ES256", "SM2" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SignAndVerifyAsync_ReturnsOk()
|
||||
{
|
||||
using var client = new HttpClient(new StubHandler())
|
||||
{
|
||||
BaseAddress = new Uri("http://localhost")
|
||||
};
|
||||
|
||||
var result = await SmokeLogic.SignAndVerifyAsync(client, "SM2", "hello", CancellationToken.None);
|
||||
result.Ok.Should().BeTrue();
|
||||
result.Error.Should().BeEmpty();
|
||||
}
|
||||
|
||||
private sealed class StubHandler : HttpMessageHandler
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = request.RequestUri?.AbsolutePath ?? string.Empty;
|
||||
if (path.Equals("/sign", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(BuildJsonResponse(new { signature_b64 = "c2ln", algorithm = "SM2" }));
|
||||
}
|
||||
|
||||
if (path.Equals("/verify", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(BuildJsonResponse(new { ok = true, algorithm = "SM2" }));
|
||||
}
|
||||
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
|
||||
}
|
||||
|
||||
private static HttpResponseMessage BuildJsonResponse(object payload)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,7 @@
|
||||
<EnableTrimAnalyzer>false</EnableTrimAnalyzer>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="__Tests/**" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -116,3 +116,5 @@ static ProcessResult RunProcess(string[] args, bool allowFailure = false)
|
||||
sealed record HashRequest([property: JsonPropertyName("data_b64")] string DataBase64);
|
||||
sealed record KeysetRequest([property: JsonPropertyName("name")] string? Name);
|
||||
sealed record ProcessResult(int ExitCode, string Output);
|
||||
|
||||
public partial class Program { }
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\CryptoProLinuxApi.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
|
||||
namespace CryptoProLinuxApi.Tests;
|
||||
|
||||
public sealed class CryptoProLinuxApiTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public CryptoProLinuxApiTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_client = factory.CreateClient();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Health_ReportsStatus()
|
||||
{
|
||||
var response = await _client.GetAsync("/health");
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||
doc.RootElement.GetProperty("status").GetString().Should().Be("ok");
|
||||
doc.RootElement.GetProperty("csptest").GetString().Should().NotBeNullOrWhiteSpace();
|
||||
return;
|
||||
}
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
body.Contains("csptest", StringComparison.OrdinalIgnoreCase).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task License_ReturnsResultShape()
|
||||
{
|
||||
var response = await _client.GetAsync("/license");
|
||||
response.IsSuccessStatusCode.Should().BeTrue();
|
||||
|
||||
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||
doc.RootElement.GetProperty("exitCode").ValueKind.Should().Be(JsonValueKind.Number);
|
||||
doc.RootElement.GetProperty("output").ValueKind.Should().Be(JsonValueKind.String);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Hash_InvalidBase64_ReturnsBadRequest()
|
||||
{
|
||||
var response = await _client.PostAsJsonAsync("/hash", new { data_b64 = "not-base64" });
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Hash_ValidBase64_ReturnsResultShape()
|
||||
{
|
||||
var payload = Convert.ToBase64String(Encoding.UTF8.GetBytes("test"));
|
||||
var response = await _client.PostAsJsonAsync("/hash", new { data_b64 = payload });
|
||||
response.IsSuccessStatusCode.Should().BeTrue();
|
||||
|
||||
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||
doc.RootElement.GetProperty("exitCode").ValueKind.Should().Be(JsonValueKind.Number);
|
||||
doc.RootElement.GetProperty("output").ValueKind.Should().Be(JsonValueKind.String);
|
||||
doc.RootElement.GetProperty("digest_b64").ValueKind.Should().BeOneOf(JsonValueKind.Null, JsonValueKind.String);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task KeysetInit_ReturnsResultShape()
|
||||
{
|
||||
var response = await _client.PostAsJsonAsync("/keyset/init", new { name = "test" });
|
||||
response.IsSuccessStatusCode.Should().BeTrue();
|
||||
|
||||
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||
doc.RootElement.GetProperty("exitCode").ValueKind.Should().Be(JsonValueKind.Number);
|
||||
doc.RootElement.GetProperty("output").ValueKind.Should().Be(JsonValueKind.String);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
global using Xunit;
|
||||
@@ -0,0 +1 @@
|
||||
global using Xunit;
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Xml.Linq;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace NugetPrime.Tests;
|
||||
|
||||
public sealed class NugetPrimeTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("nuget-prime.csproj")]
|
||||
[InlineData("nuget-prime-v9.csproj")]
|
||||
public void PackageDownloads_ArePinned(string projectFile)
|
||||
{
|
||||
var repoRoot = FindRepoRoot();
|
||||
var path = Path.Combine(repoRoot, "devops", "tools", "nuget-prime", projectFile);
|
||||
File.Exists(path).Should().BeTrue($"expected {projectFile} under devops/tools/nuget-prime");
|
||||
|
||||
var doc = XDocument.Load(path);
|
||||
var packages = doc.Descendants().Where(element => element.Name.LocalName == "PackageDownload").ToList();
|
||||
packages.Should().NotBeEmpty();
|
||||
|
||||
foreach (var package in packages)
|
||||
{
|
||||
var include = package.Attribute("Include")?.Value;
|
||||
include.Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
var version = package.Attribute("Version")?.Value;
|
||||
version.Should().NotBeNullOrWhiteSpace();
|
||||
version.Should().NotContain("*");
|
||||
}
|
||||
}
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
for (var i = 0; i < 12 && current is not null; i++)
|
||||
{
|
||||
var candidate = Path.Combine(current.FullName, "devops", "tools", "nuget-prime", "nuget-prime.csproj");
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Repo root not found for devops/tools/nuget-prime");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user