/** * uat-plan/discover-endpoints — pure controller-parse tests against representative * SmartStack controllers (nav-driven, integration-template, route-constraint). */ import { describe, it, expect } from 'vitest'; import { parseController } from '../discover-endpoints.js'; const NAV_CONTROLLER = ` using Microsoft.AspNetCore.Mvc; namespace App.Api.Controllers; [ApiController] [NavRoute("administration.users")] [Microsoft.AspNetCore.Authorization.Authorize] public class UsersController : ControllerBase { [HttpGet] [RequirePermission(Permissions.Admin.Users.View)] [ProducesResponseType(typeof(PaginatedResult), StatusCodes.Status200OK)] public async Task GetUsers([FromQuery] UserQuery q) => Ok(); [HttpGet("{id:guid}")] [RequirePermission(Permissions.Admin.Users.View)] public async Task GetUser(Guid id) => Ok(); [HttpPost] [RequirePermission(Permissions.Admin.Users.Create)] [ProducesResponseType(typeof(UserDetailDto), StatusCodes.Status201Created)] public async Task Create([FromBody] CreateUserCommand c) => Ok(); } `; const INTEGRATION_CONTROLLER = ` [ApiController] [ApiExplorerSettings(GroupName = "integration")] [Route("api/v1/integration/users")] [NavRoute("administration.users")] [Authorize] public class UsersController : ControllerBase { [HttpGet] [RequirePermission(AdministrationPermissions.Users.Read)] public IActionResult List() => Ok(); [HttpDelete("{id:guid}")] [RequirePermission(AdministrationPermissions.Users.Delete)] public IActionResult Delete(Guid id) => Ok(); } `; describe('parseController — nav-driven controller', () => { const eps = parseController(NAV_CONTROLLER); it('derives the base API route from [NavRoute] when there is no [Route]', () => { expect(eps.map((e) => `${e.method} ${e.route}`)).toEqual([ 'GET /api/administration/users', 'GET /api/administration/users/{id}', 'POST /api/administration/users', ]); }); it('captures the controller, navRoute and the raw permission expression per action', () => { expect(eps[0]).toMatchObject({ controller: 'UsersController', navRoute: 'administration.users', permissionExpr: 'Permissions.Admin.Users.View', }); expect(eps[2].permissionExpr).toBe('Permissions.Admin.Users.Create'); }); it('does not leak a permission across actions', () => { // each action's RequirePermission is its own; the POST must not inherit GET's expect(eps[1].permissionExpr).toBe('Permissions.Admin.Users.View'); expect(eps[2].permissionExpr).toBe('Permissions.Admin.Users.Create'); }); it('reads the declared success status from [ProducesResponseType] (200 list / 201 create)', () => { expect(eps[0].okStatus).toBe(200); // GET list declares Status200OK expect(eps[1].okStatus).toBeUndefined(); // GET {id} declares none here expect(eps[2].okStatus).toBe(201); // POST create declares Status201Created }); }); describe('parseController — integration controller', () => { const eps = parseController(INTEGRATION_CONTROLLER); it('prefers [NavRoute] over a literal [Route] (matches the runtime rewrite) and strips route constraints', () => { // The platform's NavigationRouteModelProvider rewrites the route from [NavRoute] to // /api/{module}/{section} and DISCARDS the [Route] — so the resolved route is the // NavRoute one, NOT the dead /api/v1/integration literal. (Generated controllers no // longer carry [Route]; this fixture keeps both to lock the parser's precedence.) expect(eps.map((e) => `${e.method} ${e.route}`)).toEqual([ 'GET /api/administration/users', 'DELETE /api/administration/users/{id}', ]); }); it('still records the navRoute for permission reconciliation', () => { expect(eps[0].navRoute).toBe('administration.users'); expect(eps[1].permissionExpr).toBe('AdministrationPermissions.Users.Delete'); }); }); describe('parseController — edges', () => { it('returns nothing for a file with no HTTP actions', () => { expect(parseController('public class Foo { }')).toEqual([]); }); it('tolerates a verb with no permission attribute', () => { const eps = parseController(` [NavRoute("public.health")] public class HealthController : ControllerBase { [HttpGet("ping")] public IActionResult Ping() => Ok(); }`); expect(eps).toHaveLength(1); expect(eps[0]).toMatchObject({ method: 'GET', route: '/api/public/health/ping' }); expect(eps[0].permissionExpr).toBeUndefined(); }); it('flags [AllowAnonymous] actions', () => { const eps = parseController(` [NavRoute("public.health")] public class HealthController : ControllerBase { [HttpGet("ping")] [AllowAnonymous] public IActionResult Ping() => Ok(); }`); expect(eps[0].allowAnonymous).toBe(true); }); it('captures the permission even when [RequirePermission] sits ABOVE the verb (order-independent block scan)', () => { const eps = parseController(` [NavRoute("a.b")] public class BController : ControllerBase { [RequirePermission(Permissions.A.B.View)] [HttpGet] public IActionResult List() => Ok(); }`); expect(eps).toHaveLength(1); expect(eps[0].permissionExpr).toBe('Permissions.A.B.View'); }); });