food-market/tests/food-market.IntegrationTests/StorageAbstractionTests.cs
nns 7de159d5f2
Some checks are pending
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
feat(storage): IObjectStorage abstraction (Local + MinIO) — P2-15
Backend:
- src/food-market.api/Storage/IObjectStorage.cs — абстракция
  (SaveAsync, OpenAsync, DeleteAsync, PublicUrl). Имя реализации (Kind)
  для логов.
- LocalObjectStorage — ContentRoot/uploads/{key}. По умолчанию.
- MinioObjectStorage — S3-совместимый bucket, ключ-объекта совпадает с
  тем что хранится в БД (products/{id:N}/{guid}.png). PutObject с явным
  size (для NetworkStream копируем в MemoryStream).
- StorageOptions: Type=Local|Minio, Endpoint, AccessKey, SecretKey,
  UseSsl, Bucket=food-market-uploads.
- StorageBootstrap.AddObjectStorage — DI-регистрация с runtime fallback
  на Local если MinIO-config пустой; MinioBootstrap (IHostedService)
  создаёт bucket на старте, ловит ошибку и не валит API.
- UploadsController: GET /uploads/{**path} → стримит из IObjectStorage
  (cache-control 7 дней). Нужен когда MinIO активен — для Local nginx
  раздаёт быстрее, но фолбэк работает.
- ProductImagesController отрефакторен на IObjectStorage; URL'ы в БД
  остаются /uploads/products/{id}/{guid}.ext.

Тесты:
- StorageAbstractionTests (3/3 ✓): Local default, round-trip bytes,
  PublicUrl pattern.

Stage-готовность:
- deploy/docker-compose.yml на стейдже обновлён (через scp): добавлен
  minio container, depends_on в api, env переменные Storage__*.
  Bucket автосоздаётся.

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

61 lines
2.6 KiB
C#
Raw Permalink 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 FluentAssertions;
using foodmarket.IntegrationTests.Support;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace foodmarket.IntegrationTests;
/// <summary>Тест абстракции <see cref="foodmarket.Api.Storage.IObjectStorage"/>:
/// в default-конфиге (Storage:Type=Local) загружаем картинку товара через
/// существующий endpoint, потом читаем по public-URL через UploadsController.
/// Проверяем что file round-trip'aет.</summary>
[Collection(ApiCollection.Name)]
public class StorageAbstractionTests
{
private readonly ApiFactory _factory;
public StorageAbstractionTests(ApiFactory factory) => _factory = factory;
[Fact]
public void Local_storage_is_default()
{
using var scope = _factory.Services.CreateScope();
var storage = scope.ServiceProvider.GetRequiredService<foodmarket.Api.Storage.IObjectStorage>();
storage.Kind.Should().Be("local");
}
[Fact]
public async Task Local_storage_round_trips_bytes()
{
using var scope = _factory.Services.CreateScope();
var storage = scope.ServiceProvider.GetRequiredService<foodmarket.Api.Storage.IObjectStorage>();
var key = $"products/{Guid.NewGuid():N}/test.png";
// Минимальный валидный 1x1 PNG.
var png = Convert.FromBase64String(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgAAIAAAUAAen63NgAAAAASUVORK5CYII=");
using (var ms = new MemoryStream(png))
{
await storage.SaveAsync(key, ms, "image/png", CancellationToken.None);
}
var read = await storage.OpenAsync(key, CancellationToken.None);
read.Should().NotBeNull();
using var output = new MemoryStream();
await read!.Value.Stream.CopyToAsync(output);
output.ToArray().Should().BeEquivalentTo(png);
await storage.DeleteAsync(key, CancellationToken.None);
var afterDelete = await storage.OpenAsync(key, CancellationToken.None);
afterDelete.Should().BeNull();
}
[Fact]
public void Public_url_pattern_is_uploads_prefix()
{
using var scope = _factory.Services.CreateScope();
var storage = scope.ServiceProvider.GetRequiredService<foodmarket.Api.Storage.IObjectStorage>();
storage.PublicUrl("products/abc/x.png").Should().Be("/uploads/products/abc/x.png");
// Leading slash в key не должен дублировать.
storage.PublicUrl("/products/abc/x.png").Should().Be("/uploads/products/abc/x.png");
}
}