food-market/tests/food-market.IntegrationTests/InvoiceUploadHttpTests.cs
nns f434e87d9b
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
fix(hotfix): invoice500 + фоновые 403 на reload + regression e2e
Owner caught on stage (2026-06-07 18:57):
1) POST /api/purchases/invoices/upload → 500: migration Phase31a создала
   invoice_uploads.UpdatedAt как NOT NULL, но конвенция проекта — DateTime?
   nullable + стамп только для Modified в AppDbContext.StampTimestamps.
   EF на INSERT отправлял UpdatedAt=NULL → 23502. Фикс: Phase31b DROP NOT NULL
   + Phase31b для invoice_aliases.
2) Фоновые 403 → тост «Нет доступа» при F5 на любой странице:
   - SavedPresets фетчил /api/user/presets (RequiresAdvancedMode) из любой
     страницы с фильтрами в простом режиме → 403;
   - useOnboardingProgress фетчил /api/organization/employees (Admin only)
     для кладовщика/кассира на '/' → 403.
   Фикс:
   - api.ts: GET-403 silent, тост только на действия юзера (POST/PUT/DELETE);
   - SavedPresets gated на advancedMode (useMe + enabled);
   - useOnboardingProgress canSeeEmployees-gate.
3) tests/food-market.IntegrationTests/InvoiceUploadHttpTests.cs — HTTP-прогон
   POST /upload через Testcontainers-Postgres (дыра: s31 pipeline tests
   проверяли matcher, но ни разу не инсертили в invoice_uploads).
4) tests/e2e/scenarios/stage-ui-menu-walk.spec.ts — regress: каждая демо-роль,
   каждый пункт меню, F5, assert 0 unexpected 4xx/5xx + 0 error-тостов +
   0 console.error. В quality-watchdog step 9 с 4-часовой каденцией.
5) tests/e2e/scenarios/stage-ui-invoice-upload.spec.ts — demo-keeper
   загружает реальное фото накладной (tests/fixtures/invoice-sample.jpg,
   сгенерирована Playwright-ом из HTML, 1800×2400 JPEG) → ждёт recognition
   → экран проверки. При не-настроенном Claude CLI тест fail'ится с ясным
   сообщением (это тоже баг).

Прогон локально 4/4 (InvoicePipelineTests 2/2 + InvoiceUploadHttpTests 2/2).
docs/hotfix-invoice500-403-progress.md — ход работы 1-4/5.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-05 00:21:41 +05:00

72 lines
3.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using FluentAssertions;
using foodmarket.IntegrationTests.Support;
using Xunit;
namespace foodmarket.IntegrationTests;
/// <summary>Регресс по hotfix-invoice500: до фикса
/// <c>POST /api/purchases/invoices/upload</c> ложился в 500 из-за
/// NOT NULL constraint на <c>UpdatedAt</c>. Тест ХОДИТ ЧЕРЕЗ HTTP на реальной
/// Postgres (Testcontainers), с мульти-файловой multipart-формой, чтобы впредь
/// заметить, если кто-то снова сделает UpdatedAt NOT NULL.</summary>
[Collection(ApiCollection.Name)]
public class InvoiceUploadHttpTests
{
private readonly ApiFactory _factory;
public InvoiceUploadHttpTests(ApiFactory factory) => _factory = factory;
[Fact]
public async Task Upload_single_file_returns_200_and_persists_row()
{
var http = _factory.CreateClient();
var actor = new ApiActor(http);
await actor.SignupAndLoginAsync($"invup-{Guid.NewGuid():N}".Substring(0, 20));
using var form = new MultipartFormDataContent();
var bytes = Encoding.UTF8.GetBytes("not a real jpeg — байты всё равно сохранятся как файл");
var filePart = new ByteArrayContent(bytes);
filePart.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg");
form.Add(filePart, "files", "invoice-test.jpg");
var resp = await http.PostAsync("/api/purchases/invoices/upload", form);
var body = await resp.Content.ReadAsStringAsync();
resp.StatusCode.Should().Be(HttpStatusCode.OK, $"body: {body}");
var json = JsonDocument.Parse(body).RootElement;
var ids = json.GetProperty("ids").EnumerateArray().Select(e => e.GetGuid()).ToList();
ids.Should().HaveCount(1);
// detail endpoint тоже должен корректно отвечать 200 (UpdatedAt null в select'е ок)
var detail = await http.GetFromJsonAsync<JsonElement>($"/api/purchases/invoices/{ids[0]}");
detail.GetProperty("fileName").GetString().Should().Be("invoice-test.jpg");
detail.GetProperty("status").GetString().Should().Be("pending");
}
[Fact]
public async Task Upload_multiple_files_in_one_request_all_persist()
{
var http = _factory.CreateClient();
var actor = new ApiActor(http);
await actor.SignupAndLoginAsync($"invup2-{Guid.NewGuid():N}".Substring(0, 20));
using var form = new MultipartFormDataContent();
for (var i = 0; i < 3; i++)
{
var fp = new ByteArrayContent(Encoding.UTF8.GetBytes($"file-{i}"));
fp.Headers.ContentType = new MediaTypeHeaderValue("image/png");
form.Add(fp, "files", $"inv-{i}.png");
}
var resp = await http.PostAsync("/api/purchases/invoices/upload", form);
resp.StatusCode.Should().Be(HttpStatusCode.OK);
var ids = (await resp.Content.ReadFromJsonAsync<JsonElement>())
.GetProperty("ids").EnumerateArray().Select(e => e.GetGuid()).ToList();
ids.Should().HaveCount(3);
}
}