food-market/tests/food-market.IntegrationTests/HangfireAccessTests.cs
nns 8e54e2e0d6 feat(s13): security headers + rate-limits + sensitive-ops audit + session revoke + Grafana
Sprint 13 — security + observability deep. 7 пунктов чек-листа ✓.

Подробности — docs/sprint13-progress.md и docs/food-market-server-postgres-role.md.

Главное:
- food-market-server (back.food-market.kz, legacy backend) теперь
  работает на dedicated PG-роли food_market_server_app (NOSUPERUSER /
  NOCREATEDB / NOCREATEROLE / NOREPLICATION / NOBYPASSRLS) с CRUD-only
  грантами. Раньше использовался postgres-superuser с паролем 1q2w3e4r.
  Бэкап конфига сохранён, rollback одной командой.
- SecurityHeadersMiddleware навешивает CSP / X-Frame-Options DENY /
  X-Content-Type-Options nosniff / Referrer-Policy strict-origin /
  Permissions-Policy. HSTS 365d + includeSubDomains + preload.
  Те же заголовки в deploy/nginx.conf для SPA HTML.
- Rate-limit:
  • Signup-IP — 3/час + 10/день (на stage'е переопределено через
    .env RATE_SIGNUP_HOUR=30 чтобы не ломать e2e).
  • Forgot-password — per-email 3/час + per-IP 10/час.
- SensitiveOpsAudit сервис, wired в:
  • TwoFactor enroll/disable
  • Employees.Update при смене RoleId (action=AssignRole,
    payload с prev/next role + полный RolePermissions)
  • MeAccount.ChangePassword (новый endpoint)
  • MeSessions.RevokeAll (новый endpoint)
- POST /api/me/sessions/revoke-all — через
  IOpenIddictAuthorizationManager.FindBySubjectAsync + TryRevokeAsync.
  Integration-тест: refresh после revoke → 400/401.
- Hangfire dashboard — nginx-route добавлен (раньше /hangfire ловился
  SPA-fallback'ом). Фильтр SuperAdmin'ом уже был. Тест: anon/tenant →
  401/403/404.
- Grafana dashboard JSON (deploy/grafana/dashboards/food-market.json,
  9 панелей) + инструкции импорта в docs/observability.md.

Проверено на stage'е: все 6 security-заголовков видны на /;
/hangfire → 401 (закрыт); 4-я форгот → 429; stage-smoke (5 этапов) ✓.

Тесты: 68 unit + 9 integration (включая 3 новых: SessionRevokeTests,
HangfireAccessTests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 12:30:10 +05:00

49 lines
2.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Net;
using System.Net.Http.Headers;
using FluentAssertions;
using foodmarket.IntegrationTests.Support;
using Xunit;
namespace foodmarket.IntegrationTests;
/// <summary>Sprint 13: Hangfire Dashboard защищён <c>SuperAdminHangfireFilter</c>.
///
/// ВАЖНО: в тестовом окружении ApiFactory выставляет
/// <c>Hangfire__Enabled=false</c> (иначе Hangfire-сервер плодил бы свои
/// таблицы в одноразовом контейнере). Когда сервер выключен,
/// <c>app.UseHangfireDashboard</c> не вызывается → /hangfire отдаёт 404.
///
/// Поэтому в этих тестах мы проверяем «дашборд НЕ открыт без авторизации»:
/// допустимый результат — 401, 403 или 404 (любая форма «нет доступа»).
/// SuperAdmin-кейс не проверяем здесь: для него на stage есть e2e-проверка
/// (см. stage-smoke). Это компромисс ради не-зависимости теста от
/// Hangfire-сервера.</summary>
[Collection(ApiCollection.Name)]
public class HangfireAccessTests
{
private readonly ApiFactory _factory;
public HangfireAccessTests(ApiFactory factory) => _factory = factory;
[Fact]
public async Task Anonymous_hangfire_returns_unauthorized()
{
using var client = _factory.CreateClient();
var resp = await client.GetAsync("/hangfire");
// Hangfire dashboard middleware возвращает 401 без auth — это NOT a 200
// (если бы было 200 — это значит filter не сработал и /hangfire утёк).
((int)resp.StatusCode).Should().BeOneOf(401, 403, 404);
}
[Fact]
public async Task Tenant_admin_cannot_access_hangfire()
{
var actor = new ApiActor(_factory.CreateClient());
await actor.SignupAndLoginAsync($"ha-{Guid.NewGuid():N}");
var resp = await actor.Http.GetAsync("/hangfire");
// 401/403 — фильтр сработал; 404 — Hangfire выключен в тестах
// (нет UseHangfireDashboard'а). Любой из этих кодов — это «доступа нет»,
// именно то что мы хотим проверить.
((int)resp.StatusCode).Should().BeOneOf(401, 403, 404);
}
}