using System.Net; using System.Net.Http.Json; using System.Text.Json; using FluentAssertions; using foodmarket.IntegrationTests.Support; using Xunit; namespace foodmarket.IntegrationTests; /// Быстрый вход кассира по PIN (grant_type=pin) на зарегистрированном /// устройстве: PIN задаёт админ, касса входит по (точка + PIN); 5 неверных → блок. [Collection(ApiCollection.Name)] public class PinLoginTests { private readonly ApiFactory _factory; public PinLoginTests(ApiFactory factory) => _factory = factory; private async Task PinLoginAsync(HttpClient client, string pointId, string pin) => await client.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { ["grant_type"] = "pin", ["client_id"] = "food-market-web", ["retail_point_id"] = pointId, ["pin"] = pin, ["scope"] = "openid profile email roles api offline_access", })); [Fact] public async Task Cashier_logs_in_by_pin_on_assigned_point() { var admin = new ApiActor(_factory.CreateClient()); await admin.SignupAndLoginAsync($"pin-{Guid.NewGuid():N}"); var refs = await admin.LoadRefsAsync(); var point = await admin.CreateRetailPointAsync(refs.StoreId, $"Точка-{Guid.NewGuid():N}"); var roleResp = await admin.Http.PostAsJsonAsync("/api/organization/employee-roles", new { name = $"Касса-{Guid.NewGuid():N}", permissions = new { retailSalesOperate = true, cashShiftOperate = true, productsView = true }, }); roleResp.EnsureSuccessStatusCode(); var roleId = (await roleResp.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); var email = $"pin-c-{Guid.NewGuid():N}@example.kz"; var empResp = await admin.Http.PostAsJsonAsync("/api/organization/employees", new { lastName = "Кассиров", firstName = "Пётр", roleId, isActive = true, createAccount = true, email, retailPointIds = new[] { point }, }); empResp.EnsureSuccessStatusCode(); var empId = (await empResp.Content.ReadFromJsonAsync()).GetProperty("employee").GetProperty("id").GetString(); // Админ задаёт PIN. var setPin = await admin.Http.PostAsJsonAsync($"/api/organization/employees/{empId}/pin", new { pin = "4321" }); setPin.StatusCode.Should().Be(HttpStatusCode.OK); // Вход по PIN (точка + PIN) → токен. var anon = _factory.CreateClient(); using var ok = await PinLoginAsync(anon, point, "4321"); ok.StatusCode.Should().Be(HttpStatusCode.OK, await ok.Content.ReadAsStringAsync()); var token = (await ok.Content.ReadFromJsonAsync()).GetProperty("access_token").GetString(); token.Should().NotBeNullOrEmpty(); // Токен рабочий: /api/me отдаёт точку кассира. anon.DefaultRequestHeaders.Authorization = new("Bearer", token); var me = await anon.GetFromJsonAsync("/api/me"); me.GetProperty("retailPointIds").EnumerateArray().Select(x => x.GetString()).Should().Contain(point); } [Fact] public async Task Wrong_pin_is_rejected_and_locks_after_five() { var admin = new ApiActor(_factory.CreateClient()); await admin.SignupAndLoginAsync($"pinlock-{Guid.NewGuid():N}"); var refs = await admin.LoadRefsAsync(); var point = await admin.CreateRetailPointAsync(refs.StoreId, $"Точка-{Guid.NewGuid():N}"); var roleResp = await admin.Http.PostAsJsonAsync("/api/organization/employee-roles", new { name = $"Касса-{Guid.NewGuid():N}", permissions = new { cashShiftOperate = true } }); var roleId = (await roleResp.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); var email = $"pinlock-c-{Guid.NewGuid():N}@example.kz"; var empResp = await admin.Http.PostAsJsonAsync("/api/organization/employees", new { lastName = "К", firstName = "П", roleId, isActive = true, createAccount = true, email, retailPointIds = new[] { point } }); var empId = (await empResp.Content.ReadFromJsonAsync()).GetProperty("employee").GetProperty("id").GetString(); (await admin.Http.PostAsJsonAsync($"/api/organization/employees/{empId}/pin", new { pin = "1111" })).EnsureSuccessStatusCode(); var anon = _factory.CreateClient(); // 5 неверных попыток. for (int i = 0; i < 5; i++) { using var bad = await PinLoginAsync(anon, point, "0000"); bad.StatusCode.Should().Be(HttpStatusCode.BadRequest); } // Теперь точка заблокирована: даже верный PIN не проходит. using var locked = await PinLoginAsync(anon, point, "1111"); locked.StatusCode.Should().Be(HttpStatusCode.BadRequest); (await locked.Content.ReadAsStringAsync()).Should().Contain("pin_locked"); } [Fact] public async Task Duplicate_pin_on_same_point_is_rejected() { var admin = new ApiActor(_factory.CreateClient()); await admin.SignupAndLoginAsync($"pindup-{Guid.NewGuid():N}"); var refs = await admin.LoadRefsAsync(); var point = await admin.CreateRetailPointAsync(refs.StoreId, $"Точка-{Guid.NewGuid():N}"); var roleResp = await admin.Http.PostAsJsonAsync("/api/organization/employee-roles", new { name = $"Касса-{Guid.NewGuid():N}", permissions = new { cashShiftOperate = true } }); var roleId = (await roleResp.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); async Task MakeEmp(string ln) { var r = await admin.Http.PostAsJsonAsync("/api/organization/employees", new { lastName = ln, firstName = "П", roleId, isActive = true, createAccount = true, email = $"dup-{Guid.NewGuid():N}@example.kz", retailPointIds = new[] { point } }); r.EnsureSuccessStatusCode(); return (await r.Content.ReadFromJsonAsync()).GetProperty("employee").GetProperty("id").GetString()!; } var e1 = await MakeEmp("Первый"); var e2 = await MakeEmp("Второй"); (await admin.Http.PostAsJsonAsync($"/api/organization/employees/{e1}/pin", new { pin = "2468" })).EnsureSuccessStatusCode(); using var dup = await admin.Http.PostAsJsonAsync($"/api/organization/employees/{e2}/pin", new { pin = "2468" }); dup.StatusCode.Should().Be(HttpStatusCode.Conflict); } }