yavsc/src/test/Yavsc.Api.Tests/ResourceUsageApiControllerTests.cs
Paul Schneider 04c42d9380
Some checks failed
Dotnet build and test / build (push) Failing after 13m0s
Forgejo Release / release (push) Successful in 8m1s
feat(admin): port site administration to API and PostIt
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 18:47:12 +01:00

228 lines
8.7 KiB
C#

using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Yavsc.Abstract.Models.Messaging;
using Yavsc.Abstract.Resources;
using Yavsc.Api.Test.Fixtures;
using Yavsc.Server.Helpers;
using Yavsc.Tests.Shared;
namespace Yavsc.Api.Test;
[Collection("Yavsc Api")]
public sealed class ResourceUsageApiControllerTests : IClassFixture<ApiWebServerFixture>
{
private readonly ApiWebServerFixture _fixture;
public ResourceUsageApiControllerTests(ApiWebServerFixture fixture)
{
_fixture = fixture;
}
private static HttpClient NewClient(ApiWebServerFixture fixture, string subject = "alice", string scope = "api", IEnumerable<Claim>? extraClaims = null)
{
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
};
var http = new HttpClient(handler)
{
BaseAddress = new Uri(fixture.BaseAddress)
};
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", TestTokenIssuer.Issue(subject, scope, extraClaims: extraClaims));
return http;
}
[Fact]
public async Task GetCurrentUserUsage_persists_snapshot_in_database()
{
using var http = NewClient(_fixture, "alice");
var response = await http.GetAsync("/api/v1/resource-usage/me", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var summary = await response.Content.ReadFromJsonAsync<ResourceUsageSummary>(TestContext.Current.CancellationToken);
Assert.NotNull(summary);
Assert.Equal("alice", summary!.UserId);
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<Yavsc.Models.ApplicationDbContext>();
Assert.Contains(db.ResourceUsageRecords, r => r.UserId == "alice");
}
[Fact]
public async Task RecordAsync_persists_cpu_and_storage_usage()
{
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<Yavsc.Models.ApplicationDbContext>();
await ResourceUsageTracker.RecordAsync(
db,
"alice",
"alice",
cpuSeconds: 1.5m,
storageBytes: 2 * 1024 * 1024,
actorUserId: "alice");
var record = await db.ResourceUsageRecords
.SingleAsync(r => r.UserId == "alice");
Assert.True(record.CpuSeconds >= 1.5m);
Assert.True(record.StorageMb >= 2m / 1024m / 1024m);
}
[Fact]
public async Task RecordAsync_persists_api_and_bandwidth_usage()
{
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<Yavsc.Models.ApplicationDbContext>();
await ResourceUsageTracker.RecordAsync(
db,
"telemetry-user",
"telemetry-user",
apiCalls: 2m,
bandwidthMb: 3.5m,
actorUserId: "telemetry-user");
var record = await db.ResourceUsageRecords
.SingleAsync(r => r.UserId == "telemetry-user");
Assert.True(record.ApiCalls >= 2m);
Assert.True(record.BandwidthMb >= 3.5m);
}
[Fact]
public async Task GetCurrentUserUsage_records_telemetry_values_from_authenticated_requests()
{
using var http = NewClient(_fixture, "telemetry-user");
var response = await http.GetAsync("/api/v1/resource-usage/me", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<Yavsc.Models.ApplicationDbContext>();
var record = await db.ResourceUsageRecords
.SingleAsync(r => r.UserId == "telemetry-user");
Assert.True(record.ApiCalls >= 1m);
Assert.True(record.BandwidthMb > 0m);
}
[Fact]
public async Task GetCurrentUserNotifications_returns_public_and_targeted_notifications()
{
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<Yavsc.Models.ApplicationDbContext>();
db.Notification.RemoveRange(db.Notification);
await db.Notification.AddRangeAsync(
new Notification
{
title = "Public",
body = "Seen by everyone",
click_action = "/public",
Target = null,
},
new Notification
{
title = "User",
body = "Private to current user",
click_action = "/profile",
Target = "user/telemetry-user",
},
new Notification
{
title = "Admin",
body = "Restricted to administrators",
click_action = "/admin",
Target = "administration",
});
await db.SaveChangesAsync();
using var http = NewClient(_fixture, "telemetry-user");
var response = await http.GetAsync("/api/v1/notifications/me", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var notifications = await response.Content.ReadFromJsonAsync<List<Notification>>(TestContext.Current.CancellationToken);
Assert.NotNull(notifications);
Assert.Contains(notifications!, n => n.title == "Public");
Assert.Contains(notifications!, n => n.title == "User");
Assert.DoesNotContain(notifications!, n => n.title == "Admin");
}
[Fact]
public async Task GetAdminOverview_returns_latest_usage_snapshot_for_each_user()
{
using var http = NewClient(_fixture, "alice");
var first = await http.GetAsync("/api/v1/resource-usage/me", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, first.StatusCode);
using var adminHttp = NewClient(
_fixture,
"admin-user",
"api",
new[]
{
new Claim(Yavsc.Constants.RoleClaimType, Yavsc.Constants.AdminGroupName)
});
var response = await adminHttp.GetAsync("/api/v1/resource-usage/admin", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<List<ResourceUsageSummary>>(TestContext.Current.CancellationToken);
Assert.NotNull(payload);
Assert.Contains(payload!, item => item.UserId == "alice");
}
[Theory]
[InlineData("Moderator", HttpStatusCode.Forbidden)]
[InlineData("Administrator", HttpStatusCode.OK)]
[InlineData("Performer", HttpStatusCode.Forbidden)]
[InlineData("moderator", HttpStatusCode.Forbidden)]
[InlineData("", HttpStatusCode.Forbidden)]
public async Task Administration_read_endpoints_enforce_roles(string role, HttpStatusCode expected)
{
using var http = NewClient(_fixture, "reader", extraClaims:
[new Claim(Yavsc.Constants.RoleClaimType, role)]);
foreach (var path in new[] { "/api/v1/users", "/api/v1/users/search/alice", "/api/v1/resource-usage/admin" })
{
var response = await http.GetAsync(path, TestContext.Current.CancellationToken);
Assert.Equal(expected, response.StatusCode);
}
}
[Fact]
public async Task Moderator_cannot_access_administrator_only_user_operations()
{
using var http = NewClient(_fixture, "moderator", extraClaims:
[new Claim(Yavsc.Constants.RoleClaimType, Yavsc.Constants.BlogModeratorGroupName)]);
foreach (var method in new[] { HttpMethod.Get, HttpMethod.Put, HttpMethod.Delete, HttpMethod.Post })
{
using var request = new HttpRequestMessage(method,
method == HttpMethod.Post ? "/api/v1/users" : "/api/v1/users/alice");
using var response = await http.SendAsync(request, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
}
[Fact]
public async Task Anonymous_user_cannot_read_administration_data()
{
using var http = NewClient(_fixture);
http.DefaultRequestHeaders.Authorization = null;
foreach (var path in new[] { "/api/v1/users", "/api/v1/users/search/alice", "/api/v1/resource-usage/admin" })
{
using var response = await http.GetAsync(path, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
}
}