< Summary

Information
Class: Chronicis.Api.Services.QuestUpdateService
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/QuestUpdateService.cs
Line coverage
93%
Covered lines: 125
Uncovered lines: 8
Coverable lines: 133
Total lines: 262
Line coverage: 93.9%
Branch coverage
73%
Covered branches: 38
Total branches: 52
Branch coverage: 73%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetQuestUpdatesAsync()78.57%141495.91%
CreateQuestUpdateAsync()66.66%313091.37%
DeleteQuestUpdateAsync()87.5%8895.45%

File(s)

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

#LineLine coverage
 1using Chronicis.Api.Data;
 2using Chronicis.Api.Models;
 3using Chronicis.Shared.DTOs.Quests;
 4using Chronicis.Shared.Enums;
 5using Chronicis.Shared.Models;
 6using Microsoft.EntityFrameworkCore;
 7
 8namespace Chronicis.Api.Services;
 9
 10/// <summary>
 11/// Service for QuestUpdate operations with world membership authorization.
 12/// </summary>
 13public class QuestUpdateService : IQuestUpdateService
 14{
 15    private readonly ChronicisDbContext _context;
 16    private readonly ILogger<QuestUpdateService> _logger;
 17
 1718    public QuestUpdateService(ChronicisDbContext context, ILogger<QuestUpdateService> logger)
 19    {
 1720        _context = context;
 1721        _logger = logger;
 1722    }
 23
 24    public async Task<ServiceResult<PagedResult<QuestUpdateEntryDto>>> GetQuestUpdatesAsync(
 25        Guid questId,
 26        Guid userId,
 27        int skip = 0,
 28        int take = 20)
 29    {
 30        // Validate pagination parameters
 631        if (skip < 0)
 32        {
 133            return ServiceResult<PagedResult<QuestUpdateEntryDto>>.ValidationError("Skip must be non-negative");
 34        }
 35
 536        if (take < 1 || take > 100)
 37        {
 138            return ServiceResult<PagedResult<QuestUpdateEntryDto>>.ValidationError("Take must be between 1 and 100");
 39        }
 40
 41        // Find quest and check access
 442        var quest = await _context.Quests
 443            .AsNoTracking()
 444            .Include(q => q.Arc)
 445                .ThenInclude(a => a.Campaign)
 446            .FirstOrDefaultAsync(q => q.Id == questId);
 47
 448        if (quest == null)
 49        {
 150            return ServiceResult<PagedResult<QuestUpdateEntryDto>>.NotFound("Quest not found");
 51        }
 52
 53        // Check world membership
 354        var member = await _context.WorldMembers
 355            .AsNoTracking()
 356            .FirstOrDefaultAsync(m => m.WorldId == quest.Arc.Campaign.WorldId && m.UserId == userId);
 57
 358        if (member == null)
 59        {
 060            return ServiceResult<PagedResult<QuestUpdateEntryDto>>.NotFound("Quest not found");
 61        }
 62
 363        var isGM = member.Role == WorldRole.GM;
 64
 65        // Non-GM users cannot see GM-only quests
 366        if (quest.IsGmOnly && !isGM)
 67        {
 068            return ServiceResult<PagedResult<QuestUpdateEntryDto>>.NotFound("Quest not found");
 69        }
 70
 71        // Get total count
 372        var totalCount = await _context.QuestUpdates
 373            .CountAsync(qu => qu.QuestId == questId);
 74
 75        // Get paginated updates
 376        var updates = await _context.QuestUpdates
 377            .AsNoTracking()
 378            .Where(qu => qu.QuestId == questId)
 379            .OrderByDescending(qu => qu.CreatedAt)
 380            .Skip(skip)
 381            .Take(take)
 382            .Select(qu => new QuestUpdateEntryDto
 383            {
 384                Id = qu.Id,
 385                QuestId = qu.QuestId,
 386                Body = qu.Body,
 387                SessionId = qu.SessionId,
 388                SessionTitle = qu.Session != null ? qu.Session.Title : null,
 389                CreatedBy = qu.CreatedBy,
 390                CreatedByName = qu.Creator.DisplayName,
 391                CreatedByAvatarUrl = qu.Creator.AvatarUrl,
 392                CreatedAt = qu.CreatedAt
 393            })
 394            .ToListAsync();
 95
 396        var result = new PagedResult<QuestUpdateEntryDto>
 397        {
 398            Items = updates,
 399            TotalCount = totalCount,
 3100            Skip = skip,
 3101            Take = take
 3102        };
 103
 3104        return ServiceResult<PagedResult<QuestUpdateEntryDto>>.Success(result);
 6105    }
 106
 107    public async Task<ServiceResult<QuestUpdateEntryDto>> CreateQuestUpdateAsync(
 108        Guid questId,
 109        QuestUpdateCreateDto dto,
 110        Guid userId)
 111    {
 112        // Validate input
 7113        if (string.IsNullOrWhiteSpace(dto.Body))
 114        {
 1115            return ServiceResult<QuestUpdateEntryDto>.ValidationError("Body is required and cannot be empty");
 116        }
 117
 118        // Find quest and check access
 6119        var quest = await _context.Quests
 6120            .Include(q => q.Arc)
 6121                .ThenInclude(a => a.Campaign)
 6122            .FirstOrDefaultAsync(q => q.Id == questId);
 123
 6124        if (quest == null)
 125        {
 0126            return ServiceResult<QuestUpdateEntryDto>.NotFound("Quest not found");
 127        }
 128
 129        // Check world membership and role
 6130        var member = await _context.WorldMembers
 6131            .AsNoTracking()
 6132            .FirstOrDefaultAsync(m => m.WorldId == quest.Arc.Campaign.WorldId && m.UserId == userId);
 133
 6134        if (member == null)
 135        {
 0136            return ServiceResult<QuestUpdateEntryDto>.NotFound("Quest not found");
 137        }
 138
 139        // Observer cannot create updates
 6140        if (member.Role == WorldRole.Observer)
 141        {
 1142            return ServiceResult<QuestUpdateEntryDto>.Forbidden("Observers cannot create quest updates");
 143        }
 144
 5145        var isGM = member.Role == WorldRole.GM;
 146
 147        // Non-GM users cannot update GM-only quests
 5148        if (quest.IsGmOnly && !isGM)
 149        {
 0150            return ServiceResult<QuestUpdateEntryDto>.NotFound("Quest not found");
 151        }
 152
 153        // Validate SessionId if provided
 5154        Article? session = null;
 5155        if (dto.SessionId.HasValue)
 156        {
 2157            session = await _context.Articles
 2158                .AsNoTracking()
 2159                .FirstOrDefaultAsync(a => a.Id == dto.SessionId.Value);
 160
 2161            if (session == null)
 162            {
 0163                return ServiceResult<QuestUpdateEntryDto>.ValidationError("Session not found");
 164            }
 165
 2166            if (session.Type != ArticleType.Session)
 167            {
 1168                return ServiceResult<QuestUpdateEntryDto>.ValidationError("Referenced article is not a Session");
 169            }
 170
 1171            if (session.ArcId != quest.ArcId)
 172            {
 0173                return ServiceResult<QuestUpdateEntryDto>.ValidationError("Session must belong to the same Arc as the qu
 174            }
 175        }
 176
 4177        var now = DateTime.UtcNow;
 178
 4179        var questUpdate = new QuestUpdate
 4180        {
 4181            Id = Guid.NewGuid(),
 4182            QuestId = questId,
 4183            SessionId = dto.SessionId,
 4184            Body = dto.Body.Trim(),
 4185            CreatedBy = userId,
 4186            CreatedAt = now
 4187        };
 188
 4189        _context.QuestUpdates.Add(questUpdate);
 190
 191        // Update parent Quest's UpdatedAt timestamp
 4192        quest.UpdatedAt = now;
 193
 4194        await _context.SaveChangesAsync();
 195
 4196        _logger.LogDebug("Created quest update for quest {QuestId} by user {UserId}", questId, userId);
 197
 198        // Fetch creator details for DTO
 4199        var creator = await _context.Users.FindAsync(userId);
 200
 4201        var resultDto = new QuestUpdateEntryDto
 4202        {
 4203            Id = questUpdate.Id,
 4204            QuestId = questUpdate.QuestId,
 4205            Body = questUpdate.Body,
 4206            SessionId = questUpdate.SessionId,
 4207            SessionTitle = session?.Title,
 4208            CreatedBy = questUpdate.CreatedBy,
 4209            CreatedByName = creator?.DisplayName ?? "Unknown",
 4210            CreatedByAvatarUrl = creator?.AvatarUrl,
 4211            CreatedAt = questUpdate.CreatedAt
 4212        };
 213
 4214        return ServiceResult<QuestUpdateEntryDto>.Success(resultDto);
 7215    }
 216
 217    public async Task<ServiceResult<bool>> DeleteQuestUpdateAsync(
 218        Guid questId,
 219        Guid updateId,
 220        Guid userId)
 221    {
 222        // Find the quest update
 4223        var questUpdate = await _context.QuestUpdates
 4224            .Include(qu => qu.Quest)
 4225                .ThenInclude(q => q.Arc)
 4226                    .ThenInclude(a => a.Campaign)
 4227            .FirstOrDefaultAsync(qu => qu.Id == updateId && qu.QuestId == questId);
 228
 4229        if (questUpdate == null)
 230        {
 1231            return ServiceResult<bool>.NotFound("Quest update not found");
 232        }
 233
 234        // Check world membership and role
 3235        var member = await _context.WorldMembers
 3236            .AsNoTracking()
 3237            .FirstOrDefaultAsync(m => m.WorldId == questUpdate.Quest.Arc.Campaign.WorldId && m.UserId == userId);
 238
 3239        if (member == null)
 240        {
 0241            return ServiceResult<bool>.NotFound("Quest update not found");
 242        }
 243
 3244        var isGM = member.Role == WorldRole.GM;
 245
 246        // Determine if user can delete this update
 3247        bool canDelete = isGM || questUpdate.CreatedBy == userId;
 248
 3249        if (!canDelete)
 250        {
 1251            return ServiceResult<bool>.Forbidden("You can only delete your own quest updates");
 252        }
 253
 2254        _context.QuestUpdates.Remove(questUpdate);
 2255        await _context.SaveChangesAsync();
 256
 2257        _logger.LogDebug("Deleted quest update {UpdateId} from quest {QuestId} by user {UserId}",
 2258            updateId, questId, userId);
 259
 2260        return ServiceResult<bool>.Success(true);
 4261    }
 262}