using System.Reflection; using FluentAssertions; using foodmarket.Api.Background; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace foodmarket.UnitTests; /// Sprint 28: lock-down тест для регекса . /// До Sprint 28 регекс return-type'a матчил только 1-level generic, поэтому /// контроллер с Task<ActionResult<PagedResult<Dto>>> терял endpoint'ы. /// Этот тест ловит регрессию через scan in-memory C# source-кода и проверку /// что нужные endpoint'ы найдены. public class ApiReferenceDocsJobTests { /// Tiny test-only env для GenerateAsync. Возвращает временный /// каталог как ContentRoot — внутри которого мы кладём наши тестовые /// Controllers/. private sealed class TestEnv : IWebHostEnvironment { public string EnvironmentName { get; set; } = "Test"; public string ApplicationName { get; set; } = "Test"; public string ContentRootPath { get; set; } = string.Empty; public string WebRootPath { get; set; } = string.Empty; public Microsoft.Extensions.FileProviders.IFileProvider WebRootFileProvider { get; set; } = null!; public Microsoft.Extensions.FileProviders.IFileProvider ContentRootFileProvider { get; set; } = null!; } private const string DoubleNestedController = @" using Microsoft.AspNetCore.Mvc; namespace foodmarket.Api.Controllers.Catalog; [Route(""api/test"")] public class TestController : ControllerBase { [HttpGet] public async Task>> List() => Ok(null); [HttpPost(""create""), RequiresPermission(""TestEdit"")] public async Task> Create() => Ok(null); [HttpDelete(""{id:guid}"")] public async Task Delete(Guid id) => NoContent(); } "; [Fact] public async Task Scans_endpoints_with_doubly_nested_generic_return() { var tmp = Directory.CreateTempSubdirectory("api-ref-test-"); try { var ctrlDir = Path.Combine(tmp.FullName, "Controllers"); Directory.CreateDirectory(ctrlDir); await File.WriteAllTextAsync(Path.Combine(ctrlDir, "TestController.cs"), DoubleNestedController); var env = new TestEnv { ContentRootPath = tmp.FullName }; var job = new ApiReferenceDocsJob(env, NullLogger.Instance); var count = await job.GenerateAsync(); count.Should().Be(3, "three endpoints: GET / POST create / DELETE {id}"); // Проверяем, что output-файл содержит все три route'а. var outFile = Path.Combine(tmp.FullName, "api-reference-generated.md"); File.Exists(outFile).Should().BeTrue(); var content = await File.ReadAllTextAsync(outFile); content.Should().Contain("/api/test/create"); content.Should().Contain("/api/test/{id:guid}"); // base-route — fallback на просто "GET /api/test". content.Should().Contain("/api/test"); // RequiresPermission('TestEdit') должен попасть в Permission колонку. content.Should().Contain("TestEdit"); } finally { Directory.Delete(tmp.FullName, recursive: true); } } }