food-market/tests/food-market.IntegrationTests/AdvancedModeTests.cs
nns 7f8750f30f
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
test(s29): unit (6) + integration (7) + e2e scenario для simple-mode (шаг 7/8)
Unit — AdvancedModeFilterTests (6/6, ~3с):
- allow when IsAdvancedMode=true
- 403 ProblemDetails с code=advanced-mode-required when simple
- SuperAdmin bypass
- InvalidateCache делает следующее чтение свежим
- unauthenticated — не трогаем ([Authorize] выше)
- authenticated без org — fail-closed 403

Integration — AdvancedModeTests (7/7, ~25с):
- /api/me.advancedMode=false по умолчанию
- GET на скрытый endpoint в простом режиме → 403 ProblemDetails
- org-admin не может переключить → 403
- SuperAdmin переключает → 200 + audit; идемпотент: повторный → changed=false
- /api/me обновилось, demand открылся
- данные пресета сохраняются через on→off→on
- архивную org переключать нельзя → 409

Доп. фикс: ApiFactory теперь прокидывает ConnectionStrings__Default через
Environment.SetEnvironmentVariable в InitializeAsync. ConfigureAppConfiguration
опаздывает к моменту чтения строки в Program.cs:86 (var poolTunedConnString=...)
— интеграционные тесты падали с «Connection refused к 127.0.0.1:5432». env-var
читает CreateBuilder сразу, строка подключения теперь корректно берётся из
Testcontainers. Это общий фикс для всей сюиты.

E2E — tests/e2e/scenarios/stage-simple-mode.{yml,steps.ts} (7 шагов):
sm01 signup→simple default; sm02 org-admin forbidden; sm03 SuperAdmin
toggle + audit; sm04 /api/me reflects + endpoint opens; sm05 data
survives toggle cycle; sm06 archived→409; sm07 Playwright UI показывает
страницу «Недоступно в простом режиме».

Quality-watchdog: GET /api/catalog/price-types не гейтится (только
мутации) — патч смока не требуется.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-04 12:02:44 +05:00

205 lines
9.1 KiB
C#
Raw Permalink 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 System.Net.Http.Json;
using System.Text.Json;
using FluentAssertions;
using foodmarket.IntegrationTests.Support;
using Xunit;
namespace foodmarket.IntegrationTests;
/// <summary>Sprint 29 — Simple/Advanced mode: integration-тесты по чек-листу
/// задачи:
/// <list type="bullet">
/// <item>простая org → GET на скрытый эндпоинт возвращает 403 + ProblemDetails;</item>
/// <item>org-admin не может переключить режим (403);</item>
/// <item>SuperAdmin переключает → 200 + запись в SuperAdminAuditLog;</item>
/// <item><c>/api/me</c> отдаёт корректный <c>advancedMode</c>;</item>
/// <item>данные скрытого модуля сохраняются после выкл→вкл.</item>
/// </list>
/// </summary>
[Collection(ApiCollection.Name)]
public class AdvancedModeTests
{
private readonly ApiFactory _factory;
public AdvancedModeTests(ApiFactory factory) => _factory = factory;
private const string SuperAdminEmail = "admin@food-market.local";
private const string SuperAdminPassword = "Admin12345!";
private async Task<ApiActor> LoginSuperAdminAsync()
{
var sa = new ApiActor(_factory.CreateClient());
sa.UseToken(await sa.TokenAsync(SuperAdminEmail, SuperAdminPassword));
return sa;
}
private static async Task<string> OrgIdFromMeAsync(ApiActor actor)
{
var me = await actor.GetJsonAsync("/api/me");
return me.GetProperty("orgId").GetString()!;
}
[Fact]
public async Task Me_returns_advancedMode_false_by_default()
{
var admin = new ApiActor(_factory.CreateClient());
await admin.SignupAndLoginAsync($"adv-default-{Guid.NewGuid():N}");
var me = await admin.GetJsonAsync("/api/me");
me.GetProperty("advancedMode").GetBoolean().Should().BeFalse(
"новая организация стартует в простом режиме");
}
[Fact]
public async Task Simple_mode_gets_403_ProblemDetails_on_hidden_endpoint()
{
var admin = new ApiActor(_factory.CreateClient());
await admin.SignupAndLoginAsync($"adv-hidden-{Guid.NewGuid():N}");
using var resp = await admin.Http.GetAsync("/api/sales/demands");
resp.StatusCode.Should().Be(HttpStatusCode.Forbidden,
"demand-эндпоинт скрыт в простом режиме");
// ProblemDetails с нашим кодом.
var body = await resp.Content.ReadFromJsonAsync<JsonElement>();
body.GetProperty("status").GetInt32().Should().Be(403);
body.GetProperty("code").GetString().Should().Be("advanced-mode-required");
}
[Fact]
public async Task Org_admin_cannot_toggle_advanced_mode()
{
var admin = new ApiActor(_factory.CreateClient());
await admin.SignupAndLoginAsync($"adv-noadmin-{Guid.NewGuid():N}");
var orgId = await OrgIdFromMeAsync(admin);
using var resp = await admin.Http.PutAsJsonAsync(
$"/api/super-admin/organizations/{orgId}/advanced-mode",
new { enabled = true });
resp.StatusCode.Should().Be(HttpStatusCode.Forbidden,
"SuperAdmin-эндпоинт недоступен обычному админу орги");
}
[Fact]
public async Task SuperAdmin_can_toggle_and_writes_audit_entry()
{
var admin = new ApiActor(_factory.CreateClient());
await admin.SignupAndLoginAsync($"adv-toggle-{Guid.NewGuid():N}");
var orgId = await OrgIdFromMeAsync(admin);
var sa = await LoginSuperAdminAsync();
using var on = await sa.Http.PutAsJsonAsync(
$"/api/super-admin/organizations/{orgId}/advanced-mode", new { enabled = true });
on.StatusCode.Should().Be(HttpStatusCode.OK);
var payload = await on.Content.ReadFromJsonAsync<JsonElement>();
payload.GetProperty("isAdvancedMode").GetBoolean().Should().BeTrue();
payload.GetProperty("changed").GetBoolean().Should().BeTrue();
// Повторный тот же вызов — changed=false, в аудит не пишется.
using var again = await sa.Http.PutAsJsonAsync(
$"/api/super-admin/organizations/{orgId}/advanced-mode", new { enabled = true });
again.StatusCode.Should().Be(HttpStatusCode.OK);
(await again.Content.ReadFromJsonAsync<JsonElement>())
.GetProperty("changed").GetBoolean().Should().BeFalse();
// Audit: SuperAdminAuditLog содержит запись EnableAdvancedMode по этой org.
var auditList = await sa.ListAsync($"/api/super-admin/audit-log?organizationId={orgId}&pageSize=50");
auditList.Should().Contain(e =>
e.GetProperty("actionType").GetString() == "EnableAdvancedMode"
&& e.GetProperty("organizationId").GetString() == orgId);
}
[Fact]
public async Task Me_advancedMode_switches_with_toggle_and_endpoint_opens()
{
var admin = new ApiActor(_factory.CreateClient());
await admin.SignupAndLoginAsync($"adv-switch-{Guid.NewGuid():N}");
var orgId = await OrgIdFromMeAsync(admin);
// Простой режим — demand 403.
(await admin.Http.GetAsync("/api/sales/demands")).StatusCode
.Should().Be(HttpStatusCode.Forbidden);
var sa = await LoginSuperAdminAsync();
(await sa.Http.PutAsJsonAsync(
$"/api/super-admin/organizations/{orgId}/advanced-mode", new { enabled = true }))
.EnsureSuccessStatusCode();
// /api/me теперь advancedMode=true.
var me2 = await admin.GetJsonAsync("/api/me");
me2.GetProperty("advancedMode").GetBoolean().Should().BeTrue();
// Demand-эндпоинт открылся.
var open = await admin.Http.GetAsync("/api/sales/demands");
open.StatusCode.Should().NotBe(HttpStatusCode.Forbidden,
"после включения расширенного режима demand-эндпоинт доступен");
}
[Fact]
public async Task Data_in_hidden_module_survives_toggle_cycle()
{
// Поведение «данные не удаляются» проверяется через UserPreset:
// создаём пресет в advanced-режиме, выключаем → заново включаем →
// пресет на месте.
var admin = new ApiActor(_factory.CreateClient());
await admin.SignupAndLoginAsync($"adv-cycle-{Guid.NewGuid():N}");
var orgId = await OrgIdFromMeAsync(admin);
var sa = await LoginSuperAdminAsync();
(await sa.Http.PutAsJsonAsync(
$"/api/super-admin/organizations/{orgId}/advanced-mode", new { enabled = true }))
.EnsureSuccessStatusCode();
// Создаём пресет.
var presetName = $"preset-{Guid.NewGuid():N}";
using var create = await admin.Http.PostAsJsonAsync("/api/user/presets", new
{
pageKey = "products",
name = presetName,
configJson = "{\"q\":\"test\"}",
});
create.StatusCode.Should().Be(HttpStatusCode.OK);
// Выключаем режим — пресет временно недоступен (403), но не удалён.
(await sa.Http.PutAsJsonAsync(
$"/api/super-admin/organizations/{orgId}/advanced-mode", new { enabled = false }))
.EnsureSuccessStatusCode();
(await admin.Http.GetAsync("/api/user/presets?pageKey=products")).StatusCode
.Should().Be(HttpStatusCode.Forbidden);
// Снова включаем — пресет виден.
(await sa.Http.PutAsJsonAsync(
$"/api/super-admin/organizations/{orgId}/advanced-mode", new { enabled = true }))
.EnsureSuccessStatusCode();
var list = await admin.ListAsync("/api/user/presets?pageKey=products");
list.Should().Contain(e => e.GetProperty("name").GetString() == presetName,
"данные пресета сохраняются при переключении режима");
}
[Fact]
public async Task Toggle_fails_for_archived_org_with_409()
{
var admin = new ApiActor(_factory.CreateClient());
await admin.SignupAndLoginAsync($"adv-arch-{Guid.NewGuid():N}");
var orgId = await OrgIdFromMeAsync(admin);
var sa = await LoginSuperAdminAsync();
// Нужно узнать имя для подтверждения архивирования.
var detail = await sa.GetJsonAsync($"/api/super-admin/organizations/{orgId}");
var name = detail.GetProperty("name").GetString()!;
(await sa.Http.PostAsJsonAsync(
$"/api/super-admin/organizations/{orgId}/archive", new { confirmationName = name }))
.EnsureSuccessStatusCode();
using var put = await sa.Http.PutAsJsonAsync(
$"/api/super-admin/organizations/{orgId}/advanced-mode", new { enabled = true });
put.StatusCode.Should().Be(HttpStatusCode.Conflict,
"архивную org переключать нельзя");
}
}