test: ApiReferenceDocsJob regex lock-down (Sprint 28)
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

Гарантирует, что Sprint 28 fix регекса для doubly-nested generics
(Task<ActionResult<PagedResult<X>>>) не регрессирует. Создаёт временный
controller-файл с 3 endpoint'ами разных типов, прогоняет GenerateAsync,
ждёт count==3 и наличие routes в output-markdown'е.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
nns 2026-06-09 03:38:29 +05:00
parent 4534f8e36c
commit ffb8456514

View file

@ -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;
/// <summary>Sprint 28: lock-down тест для регекса <see cref="ApiReferenceDocsJob"/>.
/// До Sprint 28 регекс return-type'a матчил только 1-level generic, поэтому
/// контроллер с <c>Task&lt;ActionResult&lt;PagedResult&lt;Dto&gt;&gt;&gt;</c> терял endpoint'ы.
/// Этот тест ловит регрессию через scan in-memory C# source-кода и проверку
/// что нужные endpoint'ы найдены.</summary>
public class ApiReferenceDocsJobTests
{
/// <summary>Tiny test-only env для GenerateAsync. Возвращает временный
/// каталог как ContentRoot — внутри которого мы кладём наши тестовые
/// Controllers/.</summary>
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<ActionResult<PagedResult<EmployeeDto>>> List() => Ok(null);
[HttpPost(""create""), RequiresPermission(""TestEdit"")]
public async Task<ActionResult<EmployeeDto>> Create() => Ok(null);
[HttpDelete(""{id:guid}"")]
public async Task<IActionResult> 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<ApiReferenceDocsJob>.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);
}
}
}