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
Инцидент menu-walk 2026-10-08: demo-cashier получал 403 на GET /api/money/accounts — страница смен (/money/shifts) дёргала его для выбора кассы при открытии смены, но эндпоинт под MoneyView, а у кассира MoneyView=false («кассир не видит денег»), только CashShiftOperate. - Новый узкий GET /api/money/shifts/accounts (RequiresPermission "MoneyView,CashShiftOperate"): только активные наличные кассы + эквайринг, без остатков и банковских счетов. Кассиру достаточно для выбора кассы/эквайринга при открытии смены, но денег он по-прежнему не видит. - Веб: CashShiftsPage использует useShiftAccounts() вместо useMoneyAccounts(true). - Регресс-тесты: кассир 200 на /shifts/accounts (только нал+эквайринг) и 403 на /money/accounts; /api/me permissions для кассира (advanced-only скрыт). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
8.3 KiB
C#
151 lines
8.3 KiB
C#
using System.Net;
|
||
using System.Net.Http.Json;
|
||
using System.Text.Json;
|
||
using FluentAssertions;
|
||
using foodmarket.IntegrationTests.Support;
|
||
using Xunit;
|
||
|
||
namespace foodmarket.IntegrationTests;
|
||
|
||
[Collection(ApiCollection.Name)]
|
||
public class PermissionTests
|
||
{
|
||
private readonly ApiFactory _factory;
|
||
public PermissionTests(ApiFactory factory) => _factory = factory;
|
||
|
||
[Fact]
|
||
public async Task Custom_role_without_ProductsEdit_gets_403_on_put_product()
|
||
{
|
||
var admin = new ApiActor(_factory.CreateClient());
|
||
await admin.SignupAndLoginAsync($"permadmin-{Guid.NewGuid():N}");
|
||
|
||
// Кастомная роль: просмотр товаров, но БЕЗ права правки.
|
||
var roleResp = await admin.Http.PostAsJsonAsync("/api/organization/employee-roles", new
|
||
{
|
||
name = $"Viewer-{Guid.NewGuid():N}",
|
||
description = "view only",
|
||
permissions = new { productsView = true, productsEdit = false },
|
||
});
|
||
roleResp.EnsureSuccessStatusCode();
|
||
var roleId = (await roleResp.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("id").GetString();
|
||
|
||
// Сотрудник с логином на этой роли.
|
||
var empEmail = $"viewer-{Guid.NewGuid():N}@example.kz";
|
||
var empResp = await admin.Http.PostAsJsonAsync("/api/organization/employees", new
|
||
{
|
||
lastName = "Просмотров", firstName = "Вью", roleId,
|
||
isActive = true, createAccount = true, email = empEmail,
|
||
});
|
||
empResp.EnsureSuccessStatusCode();
|
||
var pwd = (await empResp.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("generatedPassword").GetString()!;
|
||
|
||
var viewer = new ApiActor(_factory.CreateClient());
|
||
viewer.UseToken(await viewer.TokenAsync(empEmail, pwd));
|
||
|
||
// Авторизация проверяется до биндинга тела: несуществующий id → 403, если нет права.
|
||
using var put = await viewer.Http.PutAsJsonAsync($"/api/catalog/products/{Guid.NewGuid()}", new { });
|
||
put.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||
|
||
// Чтение (ProductsView) разрешено.
|
||
using var get = await viewer.Http.GetAsync("/api/catalog/products");
|
||
get.StatusCode.Should().Be(HttpStatusCode.OK);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Admin_passes_permission_gate()
|
||
{
|
||
var admin = new ApiActor(_factory.CreateClient());
|
||
await admin.SignupAndLoginAsync($"permadmin2-{Guid.NewGuid():N}");
|
||
|
||
// Админ проходит permission-гейт (дальше — 404/400 на несуществующем товаре, но НЕ 403).
|
||
using var put = await admin.Http.PutAsJsonAsync($"/api/catalog/products/{Guid.NewGuid()}", new { });
|
||
put.StatusCode.Should().NotBe(HttpStatusCode.Forbidden);
|
||
}
|
||
|
||
/// <summary>Инцидент menu-walk 2026-10-08: кассир (CashShiftOperate без
|
||
/// MoneyView — «кассир не видит денег») получал 403 на /api/money/accounts,
|
||
/// который дёргает страница смен для выбора кассы. Узкий эндпоинт
|
||
/// /api/money/shifts/accounts доступен кассиру и отдаёт только наличные/
|
||
/// эквайринг без остатков; /api/money/accounts по-прежнему 403 (инвариант).</summary>
|
||
[Fact]
|
||
public async Task Cashier_can_list_shift_accounts_but_not_money_accounts()
|
||
{
|
||
var admin = new ApiActor(_factory.CreateClient());
|
||
await admin.SignupAndLoginAsync($"shiftacc-{Guid.NewGuid():N}");
|
||
var refs = await admin.LoadRefsAsync();
|
||
|
||
// Счета: наличная касса, эквайринг и банковский (последний кассиру не положен).
|
||
await admin.CreateMoneyAccountAsync("Касса точки", 1, refs.CurrencyId, refs.StoreId, 0m);
|
||
await admin.CreateMoneyAccountAsync("Эквайринг", 4, refs.CurrencyId, refs.StoreId, 0m);
|
||
await admin.CreateMoneyAccountAsync("Расчётный счёт", 3, refs.CurrencyId, null, 0m, accountNumber: "KZ123");
|
||
|
||
// Роль «как кассир»: CashShiftOperate без MoneyView.
|
||
var roleResp = await admin.Http.PostAsJsonAsync("/api/organization/employee-roles", new
|
||
{
|
||
name = $"Касса-{Guid.NewGuid():N}", description = "shift only",
|
||
permissions = new { retailSalesOperate = true, cashShiftOperate = true, moneyView = false, productsView = true },
|
||
});
|
||
roleResp.EnsureSuccessStatusCode();
|
||
var roleId = (await roleResp.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("id").GetString();
|
||
|
||
var email = $"cashier-{Guid.NewGuid():N}@example.kz";
|
||
var empResp = await admin.Http.PostAsJsonAsync("/api/organization/employees", new
|
||
{
|
||
lastName = "Кассиров", firstName = "Пётр", roleId,
|
||
isActive = true, createAccount = true, email,
|
||
});
|
||
empResp.EnsureSuccessStatusCode();
|
||
var pwd = (await empResp.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("generatedPassword").GetString()!;
|
||
|
||
var cashier = new ApiActor(_factory.CreateClient());
|
||
cashier.UseToken(await cashier.TokenAsync(email, pwd));
|
||
|
||
// Инвариант: модуль «Деньги» кассиру недоступен.
|
||
using var accounts = await cashier.Http.GetAsync("/api/money/accounts?pageSize=100");
|
||
accounts.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||
|
||
// Но счета для открытия смены — доступны (только нал + эквайринг, без банка).
|
||
using var shiftAccounts = await cashier.Http.GetAsync("/api/money/shifts/accounts");
|
||
shiftAccounts.StatusCode.Should().Be(HttpStatusCode.OK);
|
||
var rows = await shiftAccounts.Content.ReadFromJsonAsync<JsonElement>();
|
||
var kinds = rows.EnumerateArray().Select(r => r.GetProperty("kind").GetInt32()).ToList();
|
||
kinds.Should().OnlyContain(k => k == 1 || k == 4, "только наличные (1) и эквайринг (4)");
|
||
kinds.Should().Contain(1).And.Contain(4);
|
||
rows.EnumerateArray().Select(r => r.GetProperty("name").GetString())
|
||
.Should().NotContain("Расчётный счёт", "банковский счёт кассиру в списке смены не нужен");
|
||
}
|
||
|
||
/// <summary>Эффективные права в /api/me для кассира: ProductsView/StocksView
|
||
/// есть, advanced-only (DemandsView) скрыто (простой режим).</summary>
|
||
[Fact]
|
||
public async Task Me_permissions_for_cashier_role()
|
||
{
|
||
var admin = new ApiActor(_factory.CreateClient());
|
||
await admin.SignupAndLoginAsync($"meperm-{Guid.NewGuid():N}");
|
||
var roleResp = await admin.Http.PostAsJsonAsync("/api/organization/employee-roles", new
|
||
{
|
||
name = $"Касса-{Guid.NewGuid():N}",
|
||
permissions = new { productsView = true, stocksView = true, cashShiftOperate = true, demandsView = true },
|
||
});
|
||
roleResp.EnsureSuccessStatusCode();
|
||
var roleId = (await roleResp.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("id").GetString();
|
||
var email = $"meperm-c-{Guid.NewGuid():N}@example.kz";
|
||
var empResp = await admin.Http.PostAsJsonAsync("/api/organization/employees", new
|
||
{
|
||
lastName = "К", firstName = "П", roleId, isActive = true, createAccount = true, email,
|
||
});
|
||
empResp.EnsureSuccessStatusCode();
|
||
var pwd = (await empResp.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("generatedPassword").GetString()!;
|
||
var cashier = new ApiActor(_factory.CreateClient());
|
||
cashier.UseToken(await cashier.TokenAsync(email, pwd));
|
||
|
||
var me = await cashier.Http.GetFromJsonAsync<JsonElement>("/api/me");
|
||
var perms = me.GetProperty("permissions");
|
||
perms.GetProperty("ProductsView").GetBoolean().Should().BeTrue();
|
||
perms.GetProperty("StocksView").GetBoolean().Should().BeTrue();
|
||
perms.GetProperty("ProductsEdit").GetBoolean().Should().BeFalse();
|
||
// demandsView выдали роли, но орг в простом режиме → advanced-only скрыт.
|
||
perms.GetProperty("DemandsView").GetBoolean().Should().BeFalse();
|
||
}
|
||
}
|