< Summary

Information
Class: Chronicis.Api.Services.WorldInvitationService
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/WorldInvitationService.cs
Line coverage
97%
Covered lines: 140
Uncovered lines: 3
Coverable lines: 143
Total lines: 228
Line coverage: 97.9%
Branch coverage
78%
Covered branches: 30
Total branches: 38
Branch coverage: 78.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetInvitationsAsync()100%22100%
CreateInvitationAsync()66.66%121295.45%
RevokeInvitationAsync()75%4492.3%
JoinWorldAsync()93.75%1616100%

File(s)

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

#LineLine coverage
 1using Chronicis.Api.Data;
 2using Chronicis.Shared.DTOs;
 3using Chronicis.Shared.Enums;
 4using Chronicis.Shared.Extensions;
 5using Chronicis.Shared.Models;
 6using Microsoft.EntityFrameworkCore;
 7
 8namespace Chronicis.Api.Services;
 9
 10/// <summary>
 11/// Service for world invitation management
 12/// </summary>
 13public class WorldInvitationService : IWorldInvitationService
 14{
 15    private readonly ChronicisDbContext _context;
 16    private readonly ILogger<WorldInvitationService> _logger;
 17
 1818    public WorldInvitationService(ChronicisDbContext context, ILogger<WorldInvitationService> logger)
 19    {
 1820        _context = context;
 1821        _logger = logger;
 1822    }
 23
 24    public async Task<List<WorldInvitationDto>> GetInvitationsAsync(Guid worldId, Guid userId)
 25    {
 26        // Only GMs can view invitations
 227        var isGM = await _context.WorldMembers
 228            .AnyAsync(m => m.WorldId == worldId && m.UserId == userId && m.Role == WorldRole.GM);
 29
 230        if (!isGM)
 131            return new List<WorldInvitationDto>();
 32
 133        var invitations = await _context.WorldInvitations
 134            .Include(i => i.Creator)
 135            .Where(i => i.WorldId == worldId)
 136            .OrderByDescending(i => i.CreatedAt)
 137            .ToListAsync();
 38
 339        return invitations.Select(i => new WorldInvitationDto
 340        {
 341            Id = i.Id,
 342            WorldId = i.WorldId,
 343            Code = i.Code,
 344            Role = i.Role,
 345            CreatedBy = i.CreatedBy,
 346            CreatorName = i.Creator?.DisplayName ?? "Unknown",
 347            CreatedAt = i.CreatedAt,
 348            ExpiresAt = i.ExpiresAt,
 349            MaxUses = i.MaxUses,
 350            UsedCount = i.UsedCount,
 351            IsActive = i.IsActive
 352        }).ToList();
 253    }
 54
 55    public async Task<WorldInvitationDto?> CreateInvitationAsync(Guid worldId, WorldInvitationCreateDto dto, Guid userId
 56    {
 57        // Only GMs can create invitations
 1758        var isGM = await _context.WorldMembers
 1759            .AnyAsync(m => m.WorldId == worldId && m.UserId == userId && m.Role == WorldRole.GM);
 60
 1761        if (!isGM)
 162            return null;
 63
 64        // Generate unique code
 65        string code;
 1666        int attempts = 0;
 67        do
 68        {
 1669            code = Utilities.InvitationCodeGenerator.GenerateCode();
 1670            attempts++;
 1671        } while (await _context.WorldInvitations.AnyAsync(i => i.Code == code) && attempts < 10);
 72
 1673        if (attempts >= 10)
 74        {
 075            _logger.LogError("Failed to generate unique invitation code after 10 attempts");
 076            return null;
 77        }
 78
 1679        var invitation = new WorldInvitation
 1680        {
 1681            Id = Guid.NewGuid(),
 1682            WorldId = worldId,
 1683            Code = code,
 1684            Role = dto.Role,
 1685            CreatedBy = userId,
 1686            CreatedAt = DateTime.UtcNow,
 1687            ExpiresAt = dto.ExpiresAt,
 1688            MaxUses = dto.MaxUses,
 1689            UsedCount = 0,
 1690            IsActive = true
 1691        };
 92
 1693        _context.WorldInvitations.Add(invitation);
 1694        await _context.SaveChangesAsync();
 95
 1696        _logger.LogDebugSanitized("Created invitation {Code} for world {WorldId} by user {UserId}",
 1697            code, worldId, userId);
 98
 1699        var creator = await _context.Users.FindAsync(userId);
 100
 16101        return new WorldInvitationDto
 16102        {
 16103            Id = invitation.Id,
 16104            WorldId = invitation.WorldId,
 16105            Code = invitation.Code,
 16106            Role = invitation.Role,
 16107            CreatedBy = invitation.CreatedBy,
 16108            CreatorName = creator?.DisplayName ?? "Unknown",
 16109            CreatedAt = invitation.CreatedAt,
 16110            ExpiresAt = invitation.ExpiresAt,
 16111            MaxUses = invitation.MaxUses,
 16112            UsedCount = invitation.UsedCount,
 16113            IsActive = invitation.IsActive
 16114        };
 17115    }
 116
 117    public async Task<bool> RevokeInvitationAsync(Guid worldId, Guid invitationId, Guid userId)
 118    {
 119        // Only GMs can revoke invitations
 3120        var isGM = await _context.WorldMembers
 3121            .AnyAsync(m => m.WorldId == worldId && m.UserId == userId && m.Role == WorldRole.GM);
 122
 3123        if (!isGM)
 1124            return false;
 125
 2126        var invitation = await _context.WorldInvitations
 2127            .FirstOrDefaultAsync(i => i.Id == invitationId && i.WorldId == worldId);
 128
 2129        if (invitation == null)
 0130            return false;
 131
 2132        invitation.IsActive = false;
 2133        await _context.SaveChangesAsync();
 134
 2135        _logger.LogDebug("Revoked invitation {InvitationId} for world {WorldId}", invitationId, worldId);
 136
 2137        return true;
 3138    }
 139
 140    public async Task<WorldJoinResultDto> JoinWorldAsync(string code, Guid userId)
 141    {
 11142        var normalizedCode = Utilities.InvitationCodeGenerator.NormalizeCode(code);
 143
 11144        if (!Utilities.InvitationCodeGenerator.IsValidFormat(normalizedCode))
 145        {
 1146            return new WorldJoinResultDto
 1147            {
 1148                Success = false,
 1149                ErrorMessage = "Invalid invitation code format"
 1150            };
 151        }
 152
 10153        var invitation = await _context.WorldInvitations
 10154            .Include(i => i.World)
 10155            .FirstOrDefaultAsync(i => i.Code == normalizedCode && i.IsActive);
 156
 10157        if (invitation == null)
 158        {
 2159            return new WorldJoinResultDto
 2160            {
 2161                Success = false,
 2162                ErrorMessage = "Invitation not found or has been revoked"
 2163            };
 164        }
 165
 166        // Check expiration
 8167        if (invitation.ExpiresAt.HasValue && invitation.ExpiresAt.Value < DateTime.UtcNow)
 168        {
 1169            return new WorldJoinResultDto
 1170            {
 1171                Success = false,
 1172                ErrorMessage = "This invitation has expired"
 1173            };
 174        }
 175
 176        // Check max uses
 7177        if (invitation.MaxUses.HasValue && invitation.UsedCount >= invitation.MaxUses.Value)
 178        {
 1179            return new WorldJoinResultDto
 1180            {
 1181                Success = false,
 1182                ErrorMessage = "This invitation has reached its maximum number of uses"
 1183            };
 184        }
 185
 186        // Check if user is already a member
 6187        var existingMember = await _context.WorldMembers
 6188            .FirstOrDefaultAsync(m => m.WorldId == invitation.WorldId && m.UserId == userId);
 189
 6190        if (existingMember != null)
 191        {
 1192            return new WorldJoinResultDto
 1193            {
 1194                Success = false,
 1195                ErrorMessage = "You are already a member of this world"
 1196            };
 197        }
 198
 199        // Create membership
 5200        var member = new WorldMember
 5201        {
 5202            Id = Guid.NewGuid(),
 5203            WorldId = invitation.WorldId,
 5204            UserId = userId,
 5205            Role = invitation.Role,
 5206            JoinedAt = DateTime.UtcNow,
 5207            InvitedBy = invitation.CreatedBy
 5208        };
 209
 5210        _context.WorldMembers.Add(member);
 211
 212        // Increment usage count
 5213        invitation.UsedCount++;
 214
 5215        await _context.SaveChangesAsync();
 216
 5217        _logger.LogDebugSanitized("User {UserId} joined world {WorldId} via invitation {Code}",
 5218            userId, invitation.WorldId, normalizedCode);
 219
 5220        return new WorldJoinResultDto
 5221        {
 5222            Success = true,
 5223            WorldId = invitation.WorldId,
 5224            WorldName = invitation.World?.Name,
 5225            AssignedRole = invitation.Role
 5226        };
 11227    }
 228}