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
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>
205 lines
9.1 KiB
C#
205 lines
9.1 KiB
C#
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 переключать нельзя");
|
||
}
|
||
}
|