< Summary

Information
Class: Chronicis.Api.Services.ResourceProviderService
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/ResourceProviderService.cs
Line coverage
100%
Covered lines: 7
Uncovered lines: 0
Coverable lines: 7
Total lines: 166
Line coverage: 100%
Branch coverage
100%
Covered branches: 2
Total branches: 2
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

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

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/ResourceProviderService.cs

#LineLine coverage
 1using System.Text.RegularExpressions;
 2using Chronicis.Api.Data;
 3using Chronicis.Api.Repositories;
 4using Chronicis.Shared.Models;
 5using Microsoft.EntityFrameworkCore;
 6
 7namespace Chronicis.Api.Services;
 8
 9/// <summary>
 10/// Service implementation for managing resource providers with authorization.
 11/// </summary>
 12public sealed partial class ResourceProviderService : IResourceProviderService
 13{
 14
 15    private readonly IResourceProviderRepository _repository;
 16    private readonly ChronicisDbContext _context;
 17    private readonly ILogger<ResourceProviderService> _logger;
 18
 19    public ResourceProviderService(
 20        IResourceProviderRepository repository,
 21        ChronicisDbContext context,
 22        ILogger<ResourceProviderService> logger)
 23    {
 124        _repository = repository;
 125        _context = context;
 126        _logger = logger;
 127    }
 28
 29    /// <inheritdoc/>
 30    public async Task<List<ResourceProvider>> GetAllProvidersAsync()
 31    {
 32        return await _repository.GetAllProvidersAsync();
 33    }
 34
 35    /// <inheritdoc/>
 36    public async Task<List<(ResourceProvider Provider, bool IsEnabled, string LookupKey)>> GetWorldProvidersAsync(Guid w
 37    {
 38        // Verify world exists and user has access
 39        var world = await _context.Worlds
 40            .AsNoTracking()
 41            .FirstOrDefaultAsync(w => w.Id == worldId);
 42
 43        if (world == null)
 44        {
 45            _logger.LogWarningSanitized("World {WorldId} not found", worldId);
 46            throw new KeyNotFoundException($"World {worldId} not found");
 47        }
 48
 49        // Check if user is owner or member
 50        var isOwner = world.OwnerId == userId;
 51        var isMember = await _context.WorldMembers
 52            .AnyAsync(wm => wm.WorldId == worldId && wm.UserId == userId);
 53
 54        if (!isOwner && !isMember)
 55        {
 56            _logger.LogWarningSanitized("User {UserId} unauthorized to access world {WorldId}", userId, worldId);
 57            throw new UnauthorizedAccessException($"User does not have access to world {worldId}");
 58        }
 59
 60        return await _repository.GetWorldProvidersAsync(worldId);
 61    }
 62
 63    /// <inheritdoc/>
 64    public async Task<bool> SetProviderEnabledAsync(Guid worldId, string providerCode, bool enabled, Guid userId, string
 65    {
 66        // Verify world exists
 67        var world = await _context.Worlds
 68            .AsNoTracking()
 69            .FirstOrDefaultAsync(w => w.Id == worldId);
 70
 71        if (world == null)
 72        {
 73            _logger.LogWarningSanitized("World {WorldId} not found", worldId);
 74            throw new KeyNotFoundException($"World {worldId} not found");
 75        }
 76
 77        // Check if user is the owner (only owners can modify settings)
 78        if (world.OwnerId != userId)
 79        {
 80            _logger.LogWarningSanitized("User {UserId} is not owner of world {WorldId}", userId, worldId);
 81            throw new UnauthorizedAccessException($"Only the world owner can modify resource provider settings");
 82        }
 83
 84        var providerExists = await _context.ResourceProviders
 85            .AsNoTracking()
 86            .AnyAsync(rp => rp.Code == providerCode && rp.IsActive);
 87
 88        if (!providerExists)
 89        {
 90            _logger.LogWarningSanitized("Provider {ProviderCode} not found or inactive", providerCode);
 91            throw new KeyNotFoundException($"Resource provider '{providerCode}' not found or inactive");
 92        }
 93
 94        var lookupKeyUpdateRequested = lookupKey != null;
 95        var normalizedLookupKey = lookupKeyUpdateRequested
 96            ? NormalizeLookupKey(lookupKey)
 97            : null;
 98
 99        if (lookupKeyUpdateRequested && normalizedLookupKey != null && !LookupKeyRegex().IsMatch(normalizedLookupKey))
 100        {
 101            throw new ArgumentException(
 102                "Lookup key must start with a letter/number and use only lowercase letters, numbers, '-' or '_', max 50 
 103                nameof(lookupKey));
 104        }
 105
 106        var worldProviders = await _repository.GetWorldProvidersAsync(worldId);
 107        var targetProvider = worldProviders.FirstOrDefault(
 108            p => p.Provider.Code.Equals(providerCode, StringComparison.OrdinalIgnoreCase));
 109
 110        var effectiveLookupKey = lookupKeyUpdateRequested
 111            ? normalizedLookupKey ?? providerCode.ToLowerInvariant()
 112            : targetProvider == default
 113                ? providerCode.ToLowerInvariant()
 114                : targetProvider.LookupKey.ToLowerInvariant();
 115
 116        if (enabled)
 117        {
 118            var keyConflict = worldProviders.Any(p =>
 119                p.IsEnabled
 120                && !p.Provider.Code.Equals(providerCode, StringComparison.OrdinalIgnoreCase)
 121                && p.LookupKey.Equals(effectiveLookupKey, StringComparison.OrdinalIgnoreCase));
 122
 123            if (keyConflict)
 124            {
 125                throw new InvalidOperationException(
 126                    $"Lookup key '{effectiveLookupKey}' is already in use by another enabled provider in this world.");
 127            }
 128        }
 129
 130        // Attempt to enable/disable the provider
 131        var result = await _repository.SetProviderEnabledAsync(
 132            worldId,
 133            providerCode,
 134            enabled,
 135            userId,
 136            lookupKeyUpdateRequested ? lookupKey : null);
 137
 138        if (!result)
 139        {
 140            _logger.LogWarningSanitized("Provider {ProviderCode} not found or inactive", providerCode);
 141            throw new KeyNotFoundException($"Resource provider '{providerCode}' not found or inactive");
 142        }
 143
 144        _logger.LogTraceSanitized(
 145            "User {UserId} {Action} provider {ProviderCode} for world {WorldId}",
 146            userId,
 147            enabled ? "enabled" : "disabled",
 148            providerCode,
 149            worldId);
 150
 151        return true;
 152    }
 153
 154    private static string? NormalizeLookupKey(string? lookupKey)
 155    {
 4156        if (string.IsNullOrWhiteSpace(lookupKey))
 157        {
 3158            return null;
 159        }
 160
 1161        return lookupKey.Trim().ToLowerInvariant();
 162    }
 163
 164    [GeneratedRegex("^[a-z0-9][a-z0-9_-]{0,49}$")]
 165    private static partial Regex LookupKeyRegex();
 166}