From ffb845651483d27e669577f12c2d0cc6c8ef32ff Mon Sep 17 00:00:00 2001 From: nns Date: Tue, 9 Jun 2026 03:38:29 +0500 Subject: [PATCH] test: ApiReferenceDocsJob regex lock-down (Sprint 28) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Гарантирует, что Sprint 28 fix регекса для doubly-nested generics (Task>>) не регрессирует. Создаёт временный controller-файл с 3 endpoint'ами разных типов, прогоняет GenerateAsync, ждёт count==3 и наличие routes в output-markdown'е. Co-Authored-By: Claude Opus 4.7 --- .../ApiReferenceDocsJobTests.cs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/food-market.UnitTests/ApiReferenceDocsJobTests.cs diff --git a/tests/food-market.UnitTests/ApiReferenceDocsJobTests.cs b/tests/food-market.UnitTests/ApiReferenceDocsJobTests.cs new file mode 100644 index 0000000..a448f7a --- /dev/null +++ b/tests/food-market.UnitTests/ApiReferenceDocsJobTests.cs @@ -0,0 +1,81 @@ +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); + } + } +}