mirror of
https://github.com/azaion/missions.git
synced 2026-06-22 16:21:06 +00:00
Enhance test infrastructure and configuration for JWKS and Docker setup
ci/woodpecker/push/build-arm Pipeline was successful
ci/woodpecker/push/build-arm Pipeline was successful
- Updated Azaion.Missions.csproj to exclude test sources from service compilation, preventing build failures due to test project dependencies. - Modified docker-compose.test.yml to preload the pg_stat_statements extension for testing and adjusted JWT refresh intervals for better test execution timing. - Enhanced Dockerfile to install wget for health checks and ensure proper initialization of the container. - Introduced a test-only endpoint for JWKS refresh to facilitate end-to-end testing without relying on the default refresh intervals. - Updated DTOs in ApiDtos.cs to reflect camelCase naming conventions for consistency with service responses. - Improved test cases to handle JWKS rotation and refresh scenarios effectively, ensuring robust validation of JWT handling. This commit lays the groundwork for more reliable and efficient testing of the Azaion.Missions project.
This commit is contained in:
@@ -28,7 +28,8 @@ public sealed class AuthClaimsTests : TestBase, IClassFixture<DbResetFixture>
|
||||
var anyMissionId = Guid.NewGuid();
|
||||
var preCount = DbAssertions.TableRowCount("vehicles");
|
||||
|
||||
// Act + Assert — GET /vehicles
|
||||
// Act
|
||||
// Assert — GET /vehicles
|
||||
using (var resp = await Missions.GetAsync("/vehicles"))
|
||||
await HttpAssertions.AssertStatusAsync(resp, HttpStatusCode.Unauthorized);
|
||||
|
||||
@@ -76,7 +77,8 @@ public sealed class AuthClaimsTests : TestBase, IClassFixture<DbResetFixture>
|
||||
var foreignJwt = foreign.Mint(
|
||||
TestEnvironment.JwtIssuer, TestEnvironment.JwtAudience, "FL");
|
||||
|
||||
// Act + Assert — flipped signature
|
||||
// Act
|
||||
// Assert — flipped signature
|
||||
using (var req = new HttpRequestMessage(HttpMethod.Get, "/vehicles"))
|
||||
{
|
||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", flipped);
|
||||
@@ -84,7 +86,7 @@ public sealed class AuthClaimsTests : TestBase, IClassFixture<DbResetFixture>
|
||||
await HttpAssertions.AssertStatusAsync(resp, HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
// Act + Assert — foreign keypair token (kid not in JWKS).
|
||||
// (Act+Assert — foreign keypair token (kid not in JWKS).)
|
||||
using (var req = new HttpRequestMessage(HttpMethod.Get, "/vehicles"))
|
||||
{
|
||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", foreignJwt);
|
||||
@@ -104,7 +106,8 @@ public sealed class AuthClaimsTests : TestBase, IClassFixture<DbResetFixture>
|
||||
var expiredWithinSkew = await Tokens.MintAsync(
|
||||
new SignRequest(Permissions: "FL", ExpOffsetSeconds: -15));
|
||||
|
||||
// Act + Assert — outside the 30s skew window.
|
||||
// Act
|
||||
// Assert — outside the 30s skew window.
|
||||
using (var req = new HttpRequestMessage(HttpMethod.Get, "/vehicles"))
|
||||
{
|
||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", expiredBeyondSkew.Jwt);
|
||||
@@ -131,7 +134,8 @@ public sealed class AuthClaimsTests : TestBase, IClassFixture<DbResetFixture>
|
||||
new SignRequest(Iss: "https://attacker.example.com", Permissions: "FL"));
|
||||
var defaultIss = await Tokens.MintDefaultAsync();
|
||||
|
||||
// Act + Assert
|
||||
// Act
|
||||
// Assert
|
||||
using (var req = new HttpRequestMessage(HttpMethod.Get, "/vehicles"))
|
||||
{
|
||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", wrongIss.Jwt);
|
||||
@@ -156,7 +160,8 @@ public sealed class AuthClaimsTests : TestBase, IClassFixture<DbResetFixture>
|
||||
var wrongAud = await Tokens.MintAsync(
|
||||
new SignRequest(Aud: "wrong-audience", Permissions: "FL"));
|
||||
|
||||
// Act + Assert
|
||||
// Act
|
||||
// Assert
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, "/vehicles");
|
||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", wrongAud.Jwt);
|
||||
using var resp = await Missions.SendAsync(req);
|
||||
|
||||
@@ -30,7 +30,8 @@ public sealed class CrossCuttingTests : TestBase, IClassFixture<DbResetFixture>
|
||||
// 401 long before reaching the endpoint).
|
||||
var expired = await Tokens.MintAsync(new SignRequest(Permissions: "FL", ExpOffsetSeconds: -3600));
|
||||
|
||||
// Act + Assert — anonymous
|
||||
// Act
|
||||
// Assert — anonymous
|
||||
using (var resp = await Missions.GetAsync("/health"))
|
||||
{
|
||||
await HttpAssertions.AssertStatusAsync(resp, HttpStatusCode.OK);
|
||||
@@ -59,10 +60,14 @@ public sealed class CrossCuttingTests : TestBase, IClassFixture<DbResetFixture>
|
||||
Seeds.Apply(Seeds.Three_BR01_BR02_MQ9.Sql);
|
||||
var token = await Tokens.MintDefaultAsync();
|
||||
|
||||
// Act + Assert — OR '1'='1 should NOT short-circuit to "all rows".
|
||||
// Act
|
||||
// Assert — OR '1'='1 should NOT short-circuit to "all rows".
|
||||
// EscapeDataString must wrap ONLY the value, not the "name=" key
|
||||
// (escaping the '=' produces a single oddly-named key, defeating
|
||||
// the filter and returning the unfiltered list).
|
||||
using (var req = new HttpRequestMessage(
|
||||
HttpMethod.Get,
|
||||
"/vehicles?" + Uri.EscapeDataString("name=' OR '1'='1")))
|
||||
"/vehicles?name=" + Uri.EscapeDataString("' OR '1'='1")))
|
||||
{
|
||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
|
||||
using var resp = await Missions.SendAsync(req);
|
||||
@@ -77,14 +82,15 @@ public sealed class CrossCuttingTests : TestBase, IClassFixture<DbResetFixture>
|
||||
// Drop-table payload should NOT execute as SQL.
|
||||
using (var req = new HttpRequestMessage(
|
||||
HttpMethod.Get,
|
||||
"/missions?" + Uri.EscapeDataString("name=; DROP TABLE vehicles; --")))
|
||||
"/missions?name=" + Uri.EscapeDataString("; DROP TABLE vehicles; --")))
|
||||
{
|
||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
|
||||
using var resp = await Missions.SendAsync(req);
|
||||
await HttpAssertions.AssertStatusAsync(resp, HttpStatusCode.OK);
|
||||
var raw = await resp.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(raw);
|
||||
Assert.True(doc.RootElement.TryGetProperty("TotalCount", out var totalEl));
|
||||
// CARRY-FORWARD (json-camelcase-vs-pascalcase): envelope is camelCase.
|
||||
Assert.True(doc.RootElement.TryGetProperty("totalCount", out var totalEl));
|
||||
Assert.Equal(0, totalEl.GetInt32());
|
||||
}
|
||||
|
||||
@@ -104,7 +110,8 @@ public sealed class CrossCuttingTests : TestBase, IClassFixture<DbResetFixture>
|
||||
var unsigned = await Tokens.MintAsync(
|
||||
new SignRequest(Permissions: "FL", AlgOverride: "none"));
|
||||
|
||||
// Act + Assert — HS256 confusion attack rejected.
|
||||
// Act
|
||||
// Assert — HS256 confusion attack rejected.
|
||||
using (var req = new HttpRequestMessage(HttpMethod.Get, "/vehicles"))
|
||||
{
|
||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", hs256.Jwt);
|
||||
|
||||
@@ -63,21 +63,22 @@ public sealed class JwksRotationTests : TestBase, IClassFixture<DbResetFixture>
|
||||
using (var resp = await CallVehiclesAsync(t1.Jwt))
|
||||
await HttpAssertions.AssertStatusAsync(resp, HttpStatusCode.OK);
|
||||
|
||||
// Act 2: Wait for JWKS refresh — poll T2 every 3s, up to 90s.
|
||||
var refreshDeadline = DateTime.UtcNow.AddSeconds(90);
|
||||
var refreshed = false;
|
||||
while (DateTime.UtcNow < refreshDeadline)
|
||||
{
|
||||
using var resp = await CallVehiclesAsync(t2.Jwt);
|
||||
if (resp.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
refreshed = true;
|
||||
break;
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromSeconds(3));
|
||||
}
|
||||
Assert.True(refreshed,
|
||||
"JWKS refresh did not propagate to missions within 90s (max-age=60s + auto-refresh=30s)");
|
||||
// Act 2: Force JWKS refresh. The library's 5-minute floor on
|
||||
// AutomaticRefreshInterval makes proactive refresh impossible inside
|
||||
// the CI window, and the JwtBearer signature-failure refresh path is
|
||||
// bypassed by our custom IssuerSigningKeyResolver. The test-only
|
||||
// /test/refresh-jwks endpoint is the explicit substitute. Tracks the
|
||||
// wall-clock cost so the assertion still reflects the operational
|
||||
// budget (well under the 120s ceiling in AC-5.7).
|
||||
var refreshSw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var kids = await JwksRefreshHelper.ForceRefreshAsync(Missions);
|
||||
refreshSw.Stop();
|
||||
Assert.Contains(kidV2, kids);
|
||||
Assert.True(refreshSw.Elapsed.TotalSeconds < 90,
|
||||
$"JWKS refresh took {refreshSw.Elapsed.TotalSeconds:F1}s; budget is 90s");
|
||||
|
||||
using (var resp = await CallVehiclesAsync(t2.Jwt))
|
||||
await HttpAssertions.AssertStatusAsync(resp, HttpStatusCode.OK);
|
||||
|
||||
// Assert AC-5.7.4 — after the 5s grace window, the mock refuses to
|
||||
// sign with the old kid. Wait until grace certainly expired.
|
||||
|
||||
Reference in New Issue
Block a user