< Summary

Information
Class: Chronicis.Api.Services.WorldMembershipService
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/WorldMembershipService.cs
Line coverage
100%
Covered lines: 3
Uncovered lines: 0
Coverable lines: 3
Total lines: 143
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
.ctor(...)100%11100%

File(s)

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

#LineLine coverage
 1using Chronicis.Api.Data;
 2using Chronicis.Shared.DTOs;
 3using Chronicis.Shared.Enums;
 4using Microsoft.EntityFrameworkCore;
 5
 6namespace Chronicis.Api.Services;
 7
 8/// <summary>
 9/// Service for world membership and access control
 10/// </summary>
 11public sealed class WorldMembershipService : IWorldMembershipService
 12{
 13    private readonly ChronicisDbContext _context;
 14    private readonly ILogger<WorldMembershipService> _logger;
 15
 16    public WorldMembershipService(ChronicisDbContext context, ILogger<WorldMembershipService> logger)
 17    {
 1518        _context = context;
 1519        _logger = logger;
 1520    }
 21
 22    public async Task<bool> UserHasAccessAsync(Guid worldId, Guid userId)
 23    {
 24        return await _context.WorldMembers
 25            .AnyAsync(wm => wm.WorldId == worldId && wm.UserId == userId);
 26    }
 27
 28    public async Task<bool> UserOwnsWorldAsync(Guid worldId, Guid userId)
 29    {
 30        return await _context.Worlds
 31            .AnyAsync(w => w.Id == worldId && w.OwnerId == userId);
 32    }
 33
 34    public async Task<List<WorldMemberDto>> GetMembersAsync(Guid worldId, Guid userId)
 35    {
 36        // Check access
 37        if (!await UserHasAccessAsync(worldId, userId))
 38            return new List<WorldMemberDto>();
 39
 40        var members = await _context.WorldMembers
 41            .Include(m => m.User)
 42            .Include(m => m.Inviter)
 43            .Where(m => m.WorldId == worldId)
 44            .ToListAsync();
 45
 46        return members.Select(m => new WorldMemberDto
 47        {
 48            Id = m.Id,
 49            UserId = m.UserId,
 50            DisplayName = m.User?.DisplayName ?? "Unknown",
 51            Email = m.User?.Email ?? "",
 52            AvatarUrl = m.User?.AvatarUrl,
 53            Role = m.Role,
 54            JoinedAt = m.JoinedAt,
 55            InvitedBy = m.InvitedBy,
 56            InviterName = m.Inviter?.DisplayName
 57        }).ToList();
 58    }
 59
 60    public async Task<WorldMemberDto?> UpdateMemberRoleAsync(Guid worldId, Guid memberId, WorldMemberUpdateDto dto, Guid
 61    {
 62        // Only GMs can update roles
 63        var isGM = await _context.WorldMembers
 64            .AnyAsync(m => m.WorldId == worldId && m.UserId == userId && m.Role == WorldRole.GM);
 65
 66        if (!isGM)
 67            return null;
 68
 69        var member = await _context.WorldMembers
 70            .Include(m => m.User)
 71            .FirstOrDefaultAsync(m => m.Id == memberId && m.WorldId == worldId);
 72
 73        if (member == null)
 74            return null;
 75
 76        // Prevent demoting the last GM
 77        if (member.Role == WorldRole.GM && dto.Role != WorldRole.GM)
 78        {
 79            var gmCount = await _context.WorldMembers
 80                .CountAsync(m => m.WorldId == worldId && m.Role == WorldRole.GM);
 81
 82            if (gmCount <= 1)
 83            {
 84                _logger.LogWarningSanitized("Cannot demote the last GM of world {WorldId}", worldId);
 85                return null;
 86            }
 87        }
 88
 89        member.Role = dto.Role;
 90        await _context.SaveChangesAsync();
 91
 92        _logger.LogTraceSanitized("Updated member {MemberId} role to {Role} in world {WorldId}",
 93            memberId, dto.Role, worldId);
 94
 95        return new WorldMemberDto
 96        {
 97            Id = member.Id,
 98            UserId = member.UserId,
 99            DisplayName = member.User?.DisplayName ?? "Unknown",
 100            Email = member.User?.Email ?? "",
 101            AvatarUrl = member.User?.AvatarUrl,
 102            Role = member.Role,
 103            JoinedAt = member.JoinedAt,
 104            InvitedBy = member.InvitedBy
 105        };
 106    }
 107
 108    public async Task<bool> RemoveMemberAsync(Guid worldId, Guid memberId, Guid userId)
 109    {
 110        // Only GMs can remove members
 111        var isGM = await _context.WorldMembers
 112            .AnyAsync(m => m.WorldId == worldId && m.UserId == userId && m.Role == WorldRole.GM);
 113
 114        if (!isGM)
 115            return false;
 116
 117        var member = await _context.WorldMembers
 118            .FirstOrDefaultAsync(m => m.Id == memberId && m.WorldId == worldId);
 119
 120        if (member == null)
 121            return false;
 122
 123        // Prevent removing the last GM
 124        if (member.Role == WorldRole.GM)
 125        {
 126            var gmCount = await _context.WorldMembers
 127                .CountAsync(m => m.WorldId == worldId && m.Role == WorldRole.GM);
 128
 129            if (gmCount <= 1)
 130            {
 131                _logger.LogWarningSanitized("Cannot remove the last GM of world {WorldId}", worldId);
 132                return false;
 133            }
 134        }
 135
 136        _context.WorldMembers.Remove(member);
 137        await _context.SaveChangesAsync();
 138
 139        _logger.LogTraceSanitized("Removed member {MemberId} from world {WorldId}", memberId, worldId);
 140
 141        return true;
 142    }
 143}