< 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: 80
Uncovered lines: 0
Coverable lines: 80
Total lines: 143
Line coverage: 100%
Branch coverage
73%
Covered branches: 31
Total branches: 42
Branch coverage: 73.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
UserHasAccessAsync()100%11100%
UserOwnsWorldAsync()100%11100%
GetMembersAsync()100%22100%
UpdateMemberRoleAsync()75%2020100%
RemoveMemberAsync()100%88100%

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 class WorldMembershipService : IWorldMembershipService
 12{
 13    private readonly ChronicisDbContext _context;
 14    private readonly ILogger<WorldMembershipService> _logger;
 15
 1516    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    {
 724        return await _context.WorldMembers
 725            .AnyAsync(wm => wm.WorldId == worldId && wm.UserId == userId);
 726    }
 27
 28    public async Task<bool> UserOwnsWorldAsync(Guid worldId, Guid userId)
 29    {
 330        return await _context.Worlds
 331            .AnyAsync(w => w.Id == worldId && w.OwnerId == userId);
 332    }
 33
 34    public async Task<List<WorldMemberDto>> GetMembersAsync(Guid worldId, Guid userId)
 35    {
 36        // Check access
 237        if (!await UserHasAccessAsync(worldId, userId))
 138            return new List<WorldMemberDto>();
 39
 140        var members = await _context.WorldMembers
 141            .Include(m => m.User)
 142            .Include(m => m.Inviter)
 143            .Where(m => m.WorldId == worldId)
 144            .ToListAsync();
 45
 546        return members.Select(m => new WorldMemberDto
 547        {
 548            Id = m.Id,
 549            UserId = m.UserId,
 550            DisplayName = m.User?.DisplayName ?? "Unknown",
 551            Email = m.User?.Email ?? "",
 552            AvatarUrl = m.User?.AvatarUrl,
 553            Role = m.Role,
 554            JoinedAt = m.JoinedAt,
 555            InvitedBy = m.InvitedBy,
 556            InviterName = m.Inviter?.DisplayName
 557        }).ToList();
 258    }
 59
 60    public async Task<WorldMemberDto?> UpdateMemberRoleAsync(Guid worldId, Guid memberId, WorldMemberUpdateDto dto, Guid
 61    {
 62        // Only GMs can update roles
 463        var isGM = await _context.WorldMembers
 464            .AnyAsync(m => m.WorldId == worldId && m.UserId == userId && m.Role == WorldRole.GM);
 65
 466        if (!isGM)
 167            return null;
 68
 369        var member = await _context.WorldMembers
 370            .Include(m => m.User)
 371            .FirstOrDefaultAsync(m => m.Id == memberId && m.WorldId == worldId);
 72
 373        if (member == null)
 174            return null;
 75
 76        // Prevent demoting the last GM
 277        if (member.Role == WorldRole.GM && dto.Role != WorldRole.GM)
 78        {
 179            var gmCount = await _context.WorldMembers
 180                .CountAsync(m => m.WorldId == worldId && m.Role == WorldRole.GM);
 81
 182            if (gmCount <= 1)
 83            {
 184                _logger.LogWarning("Cannot demote the last GM of world {WorldId}", worldId);
 185                return null;
 86            }
 87        }
 88
 189        member.Role = dto.Role;
 190        await _context.SaveChangesAsync();
 91
 192        _logger.LogDebug("Updated member {MemberId} role to {Role} in world {WorldId}",
 193            memberId, dto.Role, worldId);
 94
 195        return new WorldMemberDto
 196        {
 197            Id = member.Id,
 198            UserId = member.UserId,
 199            DisplayName = member.User?.DisplayName ?? "Unknown",
 1100            Email = member.User?.Email ?? "",
 1101            AvatarUrl = member.User?.AvatarUrl,
 1102            Role = member.Role,
 1103            JoinedAt = member.JoinedAt,
 1104            InvitedBy = member.InvitedBy
 1105        };
 4106    }
 107
 108    public async Task<bool> RemoveMemberAsync(Guid worldId, Guid memberId, Guid userId)
 109    {
 110        // Only GMs can remove members
 4111        var isGM = await _context.WorldMembers
 4112            .AnyAsync(m => m.WorldId == worldId && m.UserId == userId && m.Role == WorldRole.GM);
 113
 4114        if (!isGM)
 1115            return false;
 116
 3117        var member = await _context.WorldMembers
 3118            .FirstOrDefaultAsync(m => m.Id == memberId && m.WorldId == worldId);
 119
 3120        if (member == null)
 1121            return false;
 122
 123        // Prevent removing the last GM
 2124        if (member.Role == WorldRole.GM)
 125        {
 1126            var gmCount = await _context.WorldMembers
 1127                .CountAsync(m => m.WorldId == worldId && m.Role == WorldRole.GM);
 128
 1129            if (gmCount <= 1)
 130            {
 1131                _logger.LogWarning("Cannot remove the last GM of world {WorldId}", worldId);
 1132                return false;
 133            }
 134        }
 135
 1136        _context.WorldMembers.Remove(member);
 1137        await _context.SaveChangesAsync();
 138
 1139        _logger.LogDebug("Removed member {MemberId} from world {WorldId}", memberId, worldId);
 140
 1141        return true;
 4142    }
 143}