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
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>
72 lines
3.2 KiB
C#
72 lines
3.2 KiB
C#
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);
|
||
}
|
||
}
|