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; /// Sprint 29 — Simple/Advanced mode: integration-тесты по чек-листу /// задачи: /// /// простая org → GET на скрытый эндпоинт возвращает 403 + ProblemDetails; /// org-admin не может переключить режим (403); /// SuperAdmin переключает → 200 + запись в SuperAdminAuditLog; /// /api/me отдаёт корректный advancedMode; /// данные скрытого модуля сохраняются после выкл→вкл. /// /// [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 LoginSuperAdminAsync() { var sa = new ApiActor(_factory.CreateClient()); sa.UseToken(await sa.TokenAsync(SuperAdminEmail, SuperAdminPassword)); return sa; } private static async Task 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(); 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(); 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()) .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 переключать нельзя"); } }