91 lines
3.3 KiB
C#
91 lines
3.3 KiB
C#
|
|
using Microsoft.AspNetCore.Http;
|
||
|
|
using Microsoft.AspNetCore.Mvc;
|
||
|
|
using Microsoft.Extensions.Configuration;
|
||
|
|
using w4c_workflows.Controllers;
|
||
|
|
using w4c_workflows.Data;
|
||
|
|
using w4c_workflows.Services.Quota;
|
||
|
|
using Xunit;
|
||
|
|
|
||
|
|
namespace w4c_workflows.Tests;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// P1-14: resetting a tenant's monthly execution counter is a platform-admin
|
||
|
|
/// action. A tenant must not be able to zero its own quota, and the gate must
|
||
|
|
/// fail closed when <c>Workflows:AdminApiKey</c> is not configured at all.
|
||
|
|
/// </summary>
|
||
|
|
[Collection("WorkflowsPostgres")]
|
||
|
|
public class WorkflowQuotaControllerTests
|
||
|
|
{
|
||
|
|
private readonly WorkflowsPostgresFixture _fixture;
|
||
|
|
|
||
|
|
public WorkflowQuotaControllerTests(WorkflowsPostgresFixture fixture) => _fixture = fixture;
|
||
|
|
|
||
|
|
private static string Tenant() => "q" + Guid.NewGuid().ToString("N")[..12];
|
||
|
|
|
||
|
|
private static IConfiguration Config(string? adminKey)
|
||
|
|
=> new ConfigurationBuilder()
|
||
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
||
|
|
{
|
||
|
|
["Workflows:AdminApiKey"] = adminKey,
|
||
|
|
})
|
||
|
|
.Build();
|
||
|
|
|
||
|
|
private static WorkflowQuotaController Controller(
|
||
|
|
WorkflowQuotaService quota, string tenantId, string? adminKey, string? providedKey)
|
||
|
|
{
|
||
|
|
var http = new DefaultHttpContext();
|
||
|
|
http.Items["TenantId"] = tenantId;
|
||
|
|
if (providedKey != null)
|
||
|
|
http.Request.Headers["X-Admin-Key"] = providedKey;
|
||
|
|
|
||
|
|
return new WorkflowQuotaController(quota, Config(adminKey))
|
||
|
|
{
|
||
|
|
ControllerContext = new ControllerContext { HttpContext = http },
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Reset_is_forbidden_when_no_admin_key_is_configured()
|
||
|
|
{
|
||
|
|
var tenantId = Tenant();
|
||
|
|
await using var db = _fixture.CreateContext();
|
||
|
|
|
||
|
|
var result = await Controller(
|
||
|
|
QuotaTestData.Service(db), tenantId, adminKey: null, providedKey: "anything").Reset(default);
|
||
|
|
|
||
|
|
var forbidden = Assert.IsType<ObjectResult>(result);
|
||
|
|
Assert.Equal(StatusCodes.Status403Forbidden, forbidden.StatusCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Reset_is_forbidden_with_the_wrong_admin_key()
|
||
|
|
{
|
||
|
|
var tenantId = Tenant();
|
||
|
|
await using var db = _fixture.CreateContext();
|
||
|
|
|
||
|
|
var result = await Controller(
|
||
|
|
QuotaTestData.Service(db), tenantId, adminKey: "correct-secret", providedKey: "wrong-secret")
|
||
|
|
.Reset(default);
|
||
|
|
|
||
|
|
var forbidden = Assert.IsType<ObjectResult>(result);
|
||
|
|
Assert.Equal(StatusCodes.Status403Forbidden, forbidden.StatusCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Reset_with_the_admin_key_zeroes_the_counter()
|
||
|
|
{
|
||
|
|
var tenantId = Tenant();
|
||
|
|
await using var db = _fixture.CreateContext();
|
||
|
|
var quota = QuotaTestData.Service(db, new WorkflowQuotaOptions { RunsPerMonth = 10 });
|
||
|
|
Assert.True(await quota.TryReserveAsync(tenantId));
|
||
|
|
Assert.Equal(1, (await quota.GetAsync(tenantId)).Used);
|
||
|
|
|
||
|
|
var controller = Controller(quota, tenantId, adminKey: "correct-secret", providedKey: "correct-secret");
|
||
|
|
|
||
|
|
var result = Assert.IsType<OkObjectResult>(await controller.Reset(default));
|
||
|
|
var snapshot = Assert.IsType<WorkflowQuota>(result.Value);
|
||
|
|
Assert.Equal(0, snapshot.Used);
|
||
|
|
Assert.Equal(0, (await quota.GetAsync(tenantId)).Used);
|
||
|
|
}
|
||
|
|
}
|