< Summary

Information
Class: Pomodoro.Web.Services.TaskService
Assembly: Pomodoro.Web
File(s): /home/runner/work/Pomodoro/Pomodoro/src/Pomodoro.Web/Services/TaskService.cs
Line coverage
100%
Covered lines: 180
Uncovered lines: 0
Coverable lines: 180
Total lines: 289
Line coverage: 100%
Branch coverage
100%
Covered branches: 60
Total branches: 60
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Tasks()100%11100%
get_AllTasks()100%11100%
get_CurrentTaskId()100%11100%
get_CurrentTask()100%11100%
.ctor(...)100%11100%
InitializeAsync()100%1010100%
ReloadAsync()100%66100%
AddTaskAsync()100%44100%
UpdateTaskAsync()100%1010100%
DeleteTaskAsync()100%88100%
CompleteTaskAsync()100%44100%
UncompleteTaskAsync()100%44100%
SelectTaskAsync()100%44100%
AddTimeToTaskAsync()100%66100%
SaveAsync()100%11100%
SaveTaskAsync()100%11100%
SaveCurrentTaskIdAsync()100%11100%
HandleTimerCompletedAsync()100%44100%
SanitizeTaskName(...)100%22100%
NotifyStateChanged()100%22100%
get_Id()100%11100%
get_CurrentTaskId()100%11100%

File(s)

/home/runner/work/Pomodoro/Pomodoro/src/Pomodoro.Web/Services/TaskService.cs

#LineLine coverage
 1using System.Web;
 2using Pomodoro.Web.Models;
 3using Pomodoro.Web.Services.Repositories;
 4
 5namespace Pomodoro.Web.Services;
 6
 7/// <summary>
 8/// Service for managing tasks using IndexedDB for persistent storage
 9/// Implements ITimerEventSubscriber to handle timer completion events
 10/// </summary>
 11public class TaskService : ITaskService, ITimerEventSubscriber
 12{
 13    private readonly ITaskRepository _taskRepository;
 14    private readonly IIndexedDbService _indexedDb;
 15    private readonly AppState _appState;
 16
 17    public event Action? OnChange;
 18
 3519    public List<TaskItem> Tasks => _appState.Tasks.Where(t => !t.IsDeleted).ToList();
 1520    public IReadOnlyList<TaskItem> AllTasks => _appState.Tasks; // Includes soft-deleted tasks for history
 1021    public Guid? CurrentTaskId => _appState.CurrentTaskId;
 222    public TaskItem? CurrentTask => _appState.CurrentTask;
 23
 5624    public TaskService(ITaskRepository taskRepository, IIndexedDbService indexedDb, AppState appState)
 5625    {
 5626        _taskRepository = taskRepository;
 5627        _indexedDb = indexedDb;
 5628        _appState = appState;
 5629    }
 30
 31    public async Task InitializeAsync()
 5632    {
 33        // Load tasks from repository
 5634        var tasks = await _taskRepository.GetAllIncludingDeletedAsync();
 5635        if (tasks != null && tasks.Count > 0)
 4436        {
 4437            _appState.Tasks = tasks;
 4438        }
 39
 40        // Load current task from app state store
 5641        var appState = await _indexedDb.GetAsync<AppStateRecord>(Constants.Storage.AppStateStore, Constants.Storage.Defa
 5642        if (appState?.CurrentTaskId.HasValue == true)
 443        {
 444            var taskId = appState.CurrentTaskId.Value;
 45            // Use thread-safe access to check if task exists
 846            if (_appState.Tasks.Any(t => t.Id == taskId))
 347            {
 348                _appState.CurrentTaskId = taskId;
 349            }
 450        }
 51
 5652        NotifyStateChanged();
 5653    }
 54
 55    /// <summary>
 56    /// Reloads all task data from storage, refreshing the in-memory cache.
 57    /// Call this after import operations to reflect changes.
 58    /// </summary>
 59    public async Task ReloadAsync()
 460    {
 61        // Reload tasks from repository
 462        var tasks = await _taskRepository.GetAllIncludingDeletedAsync();
 463        _appState.Tasks = tasks ?? new List<TaskItem>();
 64
 65        // Clear current task selection if the task no longer exists
 466        if (_appState.CurrentTaskId.HasValue)
 167        {
 268            if (!_appState.Tasks.Any(t => t.Id == _appState.CurrentTaskId.Value))
 169            {
 170                _appState.CurrentTaskId = null;
 171            }
 172        }
 73
 474        NotifyStateChanged();
 475    }
 76
 77    public async Task AddTaskAsync(string name)
 878    {
 879        var sanitized = SanitizeTaskName(name);
 80
 81        // Validate that the task name is not empty after sanitization and within length limit
 882        if (string.IsNullOrEmpty(sanitized) || sanitized.Length > Constants.UI.MaxTaskNameLength)
 383        {
 384            return;
 85        }
 86
 587        var task = new TaskItem
 588        {
 589            Id = Guid.NewGuid(),
 590            Name = sanitized,
 591            CreatedAt = DateTime.UtcNow,
 592            IsCompleted = false,
 593            TotalFocusMinutes = Constants.Tasks.InitialFocusMinutes,
 594            PomodoroCount = Constants.Tasks.InitialPomodoroCount
 595        };
 96
 97        // Insert task at beginning (thread-safe via AppState method)
 598        _appState.InsertTask(task, Constants.Tasks.InsertAtBeginning);
 99
 100        // Auto-select the new task
 5101        _appState.CurrentTaskId = task.Id;
 102
 5103        await SaveTaskAsync(task);
 5104        await SaveCurrentTaskIdAsync();
 5105        NotifyStateChanged();
 8106    }
 107
 108    public async Task UpdateTaskAsync(TaskItem task)
 9109    {
 9110        var sanitized = SanitizeTaskName(task.Name ?? string.Empty);
 111
 112        // Validate that the task name is not empty after sanitization and within length limit
 9113        if (string.IsNullOrEmpty(sanitized) || sanitized.Length > Constants.UI.MaxTaskNameLength)
 4114        {
 4115            return;
 116        }
 117
 118        // Thread-safe task update via AppState method
 9119        var updated = _appState.UpdateTask(task.Id, t => t.Name = sanitized);
 120
 5121        if (updated)
 4122        {
 4123            var updatedTask = _appState.FindTaskById(task.Id);
 4124            if (updatedTask != null)
 4125            {
 4126                await SaveTaskAsync(updatedTask);
 4127            }
 4128            NotifyStateChanged();
 4129        }
 9130    }
 131
 132    public async Task DeleteTaskAsync(Guid taskId)
 4133    {
 4134        var updated = _appState.UpdateTask(taskId, t =>
 3135        {
 4136            // Soft delete - mark as deleted but keep for history
 3137            t.IsDeleted = true;
 3138            t.DeletedAt = DateTime.UtcNow;
 7139        });
 140
 4141        if (updated)
 3142        {
 3143            if (_appState.CurrentTaskId == taskId)
 1144            {
 1145                _appState.CurrentTaskId = null;
 1146                await SaveCurrentTaskIdAsync();
 1147            }
 148
 3149            var deletedTask = _appState.FindTaskById(taskId);
 3150            if (deletedTask != null)
 3151            {
 3152                await SaveTaskAsync(deletedTask);
 3153            }
 3154            NotifyStateChanged();
 3155        }
 4156    }
 157
 158    public async Task CompleteTaskAsync(Guid taskId)
 4159    {
 7160        var updated = _appState.UpdateTask(taskId, t => t.IsCompleted = true);
 161
 4162        if (updated)
 3163        {
 3164            var task = _appState.FindTaskById(taskId);
 3165            if (task != null)
 3166            {
 3167                await SaveTaskAsync(task);
 3168            }
 3169            NotifyStateChanged();
 3170        }
 4171    }
 172
 173    public async Task UncompleteTaskAsync(Guid taskId)
 4174    {
 7175        var updated = _appState.UpdateTask(taskId, t => t.IsCompleted = false);
 176
 4177        if (updated)
 3178        {
 3179            var task = _appState.FindTaskById(taskId);
 3180            if (task != null)
 3181            {
 3182                await SaveTaskAsync(task);
 3183            }
 3184            NotifyStateChanged();
 3185        }
 4186    }
 187
 188    public async Task SelectTaskAsync(Guid taskId)
 5189    {
 5190        var task = _appState.FindTaskById(taskId);
 191
 5192        if (task != null && !task.IsCompleted)
 3193        {
 3194            _appState.CurrentTaskId = taskId;
 3195            await SaveCurrentTaskIdAsync();
 3196            NotifyStateChanged();
 3197        }
 5198    }
 199
 200    public async Task AddTimeToTaskAsync(Guid taskId, int minutes)
 8201    {
 202        // Validate that minutes is a positive value
 8203        if (minutes <= 0)
 2204        {
 2205            return;
 206        }
 207
 6208        var updated = _appState.UpdateTask(taskId, t =>
 5209        {
 5210            t.TotalFocusMinutes += minutes;
 5211            t.PomodoroCount++;
 5212            t.LastWorkedOn = DateTime.UtcNow;
 11213        });
 214
 6215        if (updated)
 5216        {
 5217            var task = _appState.FindTaskById(taskId);
 5218            if (task != null)
 5219            {
 5220                await SaveTaskAsync(task);
 5221            }
 5222            NotifyStateChanged();
 5223        }
 8224    }
 225
 226    public async Task SaveAsync()
 1227    {
 228        // Save all tasks to IndexedDB (thread-safe copy)
 1229        var tasksToSave = _appState.Tasks.ToList();
 1230        await _indexedDb.PutAllAsync(Constants.Storage.TasksStore, tasksToSave);
 1231    }
 232
 233    private async Task SaveTaskAsync(TaskItem task)
 23234    {
 23235        await _taskRepository.SaveAsync(task);
 23236    }
 237
 238    private async Task SaveCurrentTaskIdAsync()
 9239    {
 9240        var appStateRecord = new AppStateRecord
 9241        {
 9242            Id = Constants.Storage.DefaultSettingsId,
 9243            CurrentTaskId = _appState.CurrentTaskId
 9244        };
 9245        await _indexedDb.PutAsync(Constants.Storage.AppStateStore, appStateRecord);
 9246    }
 247
 248    /// <summary>
 249    /// Handles timer completion events from ITimerEventSubscriber
 250    /// Updates task time when a pomodoro completes
 251    /// </summary>
 252    public async Task HandleTimerCompletedAsync(TimerCompletedEventArgs args)
 3253    {
 254        // Only process completed pomodoro sessions with a task
 3255        if (args.SessionType != SessionType.Pomodoro || !args.TaskId.HasValue)
 2256            return;
 257
 1258        await AddTimeToTaskAsync(args.TaskId.Value, args.DurationMinutes);
 3259    }
 260
 261    /// <summary>
 262    /// Sanitizes task name by trimming and HTML-encoding to prevent XSS attacks.
 263    /// Blazor generally escapes content automatically, but this provides defense-in-depth.
 264    /// </summary>
 265    private static string SanitizeTaskName(string name)
 17266    {
 21267        if (string.IsNullOrEmpty(name)) return string.Empty;
 268
 269        // Trim whitespace
 13270        var trimmed = name.Trim();
 271
 272        // HTML encode to prevent XSS (defense-in-depth, Blazor escapes automatically)
 13273        return HttpUtility.HtmlEncode(trimmed);
 17274    }
 275
 276    private void NotifyStateChanged()
 86277    {
 86278        OnChange?.Invoke();
 86279    }
 280
 281    /// <summary>
 282    /// Record for storing app state in IndexedDB
 283    /// </summary>
 284    public class AppStateRecord
 285    {
 22286        public string Id { get; set; } = Constants.Storage.DefaultSettingsId;
 21287        public Guid? CurrentTaskId { get; set; }
 288    }
 289}