< Summary

Information
Class: Chronicis.Api.Controllers.PathsController
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Controllers/PathsController.cs
Line coverage
100%
Covered lines: 6
Uncovered lines: 0
Coverable lines: 6
Total lines: 65
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Api/Controllers/PathsController.cs

#LineLine coverage
 1using Chronicis.Api.Infrastructure;
 2using Chronicis.Api.Services.Routing;
 3using Chronicis.Shared.Routing;
 4using Chronicis.Shared.Utilities;
 5using Microsoft.AspNetCore.Authorization;
 6using Microsoft.AspNetCore.Mvc;
 7
 8namespace Chronicis.Api.Controllers;
 9
 10/// <summary>
 11/// Unified slug-path resolution endpoint. Anonymous-accessible; gating is handled by IReadAccessPolicyService.
 12/// </summary>
 13[ApiController]
 14[Route("paths")]
 15[AllowAnonymous]
 16public sealed class PathsController : ControllerBase
 17{
 118    private static readonly HashSet<string> KeywordSegments =
 119        new(StringComparer.OrdinalIgnoreCase) { "wiki", "maps" };
 20
 21    private readonly ISlugPathResolver _resolver;
 22    private readonly ICurrentUserService _currentUserService;
 23
 2124    public PathsController(ISlugPathResolver resolver, ICurrentUserService currentUserService)
 25    {
 2126        _resolver = resolver;
 2127        _currentUserService = currentUserService;
 2128    }
 29
 30    /// <summary>
 31    /// GET /paths/resolve/{*path}
 32    /// Resolves a URL path into a typed entity identity.
 33    /// Returns 200 with SlugPathResolution, 404 when not found, 400 when a segment is invalid.
 34    /// </summary>
 35    [HttpGet("resolve/{*path}")]
 36    public async Task<ActionResult<SlugPathResolution>> Resolve(
 37        string path,
 38        CancellationToken cancellationToken)
 39    {
 40        var segments = (path ?? string.Empty)
 41            .Trim('/')
 42            .Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
 43            .Select(s => s.ToLowerInvariant())
 44            .ToList();
 45
 46        if (segments.Count == 0)
 47            return NotFound();
 48
 49        // Validate each segment — keywords are exempt from slug validation
 50        for (var i = 0; i < segments.Count; i++)
 51        {
 52            var seg = segments[i];
 53            if (!KeywordSegments.Contains(seg) && !SlugGenerator.IsValidSlug(seg))
 54                return BadRequest();
 55        }
 56
 57        var currentUserId = _currentUserService.IsAuthenticated
 58            ? (Guid?)((await _currentUserService.GetCurrentUserAsync())?.Id)
 59            : null;
 60
 61        var resolution = await _resolver.ResolveAsync(segments, currentUserId, cancellationToken);
 62
 63        return resolution == null ? NotFound() : Ok(resolution);
 64    }
 65}