< Summary

Information
Class: Chronicis.Api.Controllers.WorldLinksController
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Controllers/WorldLinksController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 113
Coverable lines: 113
Total lines: 226
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 40
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
GetWorldLinks()0%620%
CreateWorldLink()0%272160%
UpdateWorldLink()0%342180%
DeleteWorldLink()0%2040%

File(s)

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

#LineLine coverage
 1using Chronicis.Api.Data;
 2using Chronicis.Api.Infrastructure;
 3using Chronicis.Shared.DTOs;
 4using Chronicis.Shared.Extensions;
 5using Chronicis.Shared.Models;
 6using Microsoft.AspNetCore.Authorization;
 7using Microsoft.AspNetCore.Mvc;
 8using Microsoft.EntityFrameworkCore;
 9
 10namespace Chronicis.Api.Controllers;
 11
 12/// <summary>
 13/// API endpoints for World Link management (external resource links).
 14/// </summary>
 15[ApiController]
 16[Route("worlds/{worldId:guid}/links")]
 17[Authorize]
 18public class WorldLinksController : ControllerBase
 19{
 20    private readonly ChronicisDbContext _db;
 21    private readonly ICurrentUserService _currentUserService;
 22    private readonly ILogger<WorldLinksController> _logger;
 23
 024    public WorldLinksController(
 025        ChronicisDbContext db,
 026        ICurrentUserService currentUserService,
 027        ILogger<WorldLinksController> logger)
 28    {
 029        _db = db;
 030        _currentUserService = currentUserService;
 031        _logger = logger;
 032    }
 33
 34    /// <summary>
 35    /// GET /worlds/{worldId}/links - Get all links for a world.
 36    /// </summary>
 37    [HttpGet]
 38    public async Task<ActionResult<IEnumerable<WorldLinkDto>>> GetWorldLinks(Guid worldId)
 39    {
 040        var user = await _currentUserService.GetRequiredUserAsync();
 041        _logger.LogDebug("Getting links for world {WorldId} by user {UserId}", worldId, user.Id);
 42
 43        // Verify user has access to the world
 044        var world = await _db.Worlds
 045            .AsNoTracking()
 046            .FirstOrDefaultAsync(w => w.Id == worldId && w.OwnerId == user.Id);
 47
 048        if (world == null)
 49        {
 050            return NotFound(new { error = "World not found or access denied" });
 51        }
 52
 053        var links = await _db.WorldLinks
 054            .AsNoTracking()
 055            .Where(wl => wl.WorldId == worldId)
 056            .OrderBy(wl => wl.Title)
 057            .Select(wl => new WorldLinkDto
 058            {
 059                Id = wl.Id,
 060                WorldId = wl.WorldId,
 061                Url = wl.Url,
 062                Title = wl.Title,
 063                Description = wl.Description,
 064                CreatedAt = wl.CreatedAt
 065            })
 066            .ToListAsync();
 67
 068        return Ok(links);
 069    }
 70
 71    /// <summary>
 72    /// POST /worlds/{worldId}/links - Create a new link for a world.
 73    /// </summary>
 74    [HttpPost]
 75    public async Task<ActionResult<WorldLinkDto>> CreateWorldLink(Guid worldId, [FromBody] WorldLinkCreateDto dto)
 76    {
 077        var user = await _currentUserService.GetRequiredUserAsync();
 78
 79        // Verify user owns the world
 080        var world = await _db.Worlds
 081            .FirstOrDefaultAsync(w => w.Id == worldId && w.OwnerId == user.Id);
 82
 083        if (world == null)
 84        {
 085            return NotFound(new { error = "World not found or access denied" });
 86        }
 87
 088        if (dto == null || string.IsNullOrWhiteSpace(dto.Url) || string.IsNullOrWhiteSpace(dto.Title))
 89        {
 090            return BadRequest(new { error = "URL and Title are required" });
 91        }
 92
 93        // Validate URL format
 094        if (!Uri.TryCreate(dto.Url, UriKind.Absolute, out var uri) ||
 095            (uri.Scheme != "http" && uri.Scheme != "https"))
 96        {
 097            return BadRequest(new { error = "Invalid URL format. Must be a valid http or https URL." });
 98        }
 99
 0100        _logger.LogDebugSanitized("Creating link '{Title}' for world {WorldId} by user {UserId}",
 0101            dto.Title, worldId, user.Id);
 102
 0103        var link = new WorldLink
 0104        {
 0105            Id = Guid.NewGuid(),
 0106            WorldId = worldId,
 0107            Url = dto.Url.Trim(),
 0108            Title = dto.Title.Trim(),
 0109            Description = string.IsNullOrWhiteSpace(dto.Description) ? null : dto.Description.Trim(),
 0110            CreatedAt = DateTime.UtcNow
 0111        };
 112
 0113        _db.WorldLinks.Add(link);
 0114        await _db.SaveChangesAsync();
 115
 0116        var result = new WorldLinkDto
 0117        {
 0118            Id = link.Id,
 0119            WorldId = link.WorldId,
 0120            Url = link.Url,
 0121            Title = link.Title,
 0122            Description = link.Description,
 0123            CreatedAt = link.CreatedAt
 0124        };
 125
 0126        return CreatedAtAction(nameof(GetWorldLinks), new { worldId }, result);
 0127    }
 128
 129    /// <summary>
 130    /// PUT /worlds/{worldId}/links/{linkId} - Update an existing world link.
 131    /// </summary>
 132    [HttpPut("{linkId:guid}")]
 133    public async Task<ActionResult<WorldLinkDto>> UpdateWorldLink(
 134        Guid worldId,
 135        Guid linkId,
 136        [FromBody] WorldLinkUpdateDto dto)
 137    {
 0138        var user = await _currentUserService.GetRequiredUserAsync();
 139
 140        // Verify user owns the world
 0141        var world = await _db.Worlds
 0142            .AsNoTracking()
 0143            .FirstOrDefaultAsync(w => w.Id == worldId && w.OwnerId == user.Id);
 144
 0145        if (world == null)
 146        {
 0147            return NotFound(new { error = "World not found or access denied" });
 148        }
 149
 0150        var link = await _db.WorldLinks
 0151            .FirstOrDefaultAsync(wl => wl.Id == linkId && wl.WorldId == worldId);
 152
 0153        if (link == null)
 154        {
 0155            return NotFound(new { error = "Link not found" });
 156        }
 157
 0158        if (dto == null || string.IsNullOrWhiteSpace(dto.Url) || string.IsNullOrWhiteSpace(dto.Title))
 159        {
 0160            return BadRequest(new { error = "URL and Title are required" });
 161        }
 162
 163        // Validate URL format
 0164        if (!Uri.TryCreate(dto.Url, UriKind.Absolute, out var uri) ||
 0165            (uri.Scheme != "http" && uri.Scheme != "https"))
 166        {
 0167            return BadRequest(new { error = "Invalid URL format. Must be a valid http or https URL." });
 168        }
 169
 0170        _logger.LogDebug("Updating link {LinkId} for world {WorldId} by user {UserId}",
 0171            linkId, worldId, user.Id);
 172
 0173        link.Url = dto.Url.Trim();
 0174        link.Title = dto.Title.Trim();
 0175        link.Description = string.IsNullOrWhiteSpace(dto.Description) ? null : dto.Description.Trim();
 176
 0177        await _db.SaveChangesAsync();
 178
 0179        var result = new WorldLinkDto
 0180        {
 0181            Id = link.Id,
 0182            WorldId = link.WorldId,
 0183            Url = link.Url,
 0184            Title = link.Title,
 0185            Description = link.Description,
 0186            CreatedAt = link.CreatedAt
 0187        };
 188
 0189        return Ok(result);
 0190    }
 191
 192    /// <summary>
 193    /// DELETE /worlds/{worldId}/links/{linkId} - Delete a world link.
 194    /// </summary>
 195    [HttpDelete("{linkId:guid}")]
 196    public async Task<IActionResult> DeleteWorldLink(Guid worldId, Guid linkId)
 197    {
 0198        var user = await _currentUserService.GetRequiredUserAsync();
 199
 200        // Verify user owns the world
 0201        var world = await _db.Worlds
 0202            .AsNoTracking()
 0203            .FirstOrDefaultAsync(w => w.Id == worldId && w.OwnerId == user.Id);
 204
 0205        if (world == null)
 206        {
 0207            return NotFound(new { error = "World not found or access denied" });
 208        }
 209
 0210        var link = await _db.WorldLinks
 0211            .FirstOrDefaultAsync(wl => wl.Id == linkId && wl.WorldId == worldId);
 212
 0213        if (link == null)
 214        {
 0215            return NotFound(new { error = "Link not found" });
 216        }
 217
 0218        _logger.LogDebug("Deleting link {LinkId} for world {WorldId} by user {UserId}",
 0219            linkId, worldId, user.Id);
 220
 0221        _db.WorldLinks.Remove(link);
 0222        await _db.SaveChangesAsync();
 223
 0224        return NoContent();
 0225    }
 226}