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);
}
}