From 5c90a6dd76be0fdf9757d984f799d0d19ce1cb0c Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 13 Aug 2026 16:43:20 -0400 Subject: [PATCH] feat(api): support task deadlines --- .../Controllers/TasksController.cs | 3 + TaskTracker.Tests/TaskTracker.Tests.csproj | 3 + .../TasksControllerDeadlineTests.cs | 91 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 TaskTracker.Tests/TasksControllerDeadlineTests.cs diff --git a/TaskTracker.Api/Controllers/TasksController.cs b/TaskTracker.Api/Controllers/TasksController.cs index 6b1ad7c..e439cb4 100644 --- a/TaskTracker.Api/Controllers/TasksController.cs +++ b/TaskTracker.Api/Controllers/TasksController.cs @@ -52,6 +52,7 @@ public class TasksController(ITaskRepository taskRepo, ILogger Category = request.Category, ParentTaskId = request.ParentTaskId, EstimatedMinutes = request.EstimatedMinutes, + DueAt = request.DueAt, Status = WorkTaskStatus.Pending }; @@ -173,6 +174,8 @@ public class TasksController(ITaskRepository taskRepo, ILogger if (request.Description is not null) task.Description = request.Description; if (request.Category is not null) task.Category = request.Category; if (request.EstimatedMinutes.HasValue) task.EstimatedMinutes = request.EstimatedMinutes; + if (request.ClearDueAt) task.DueAt = null; + else if (request.DueAt.HasValue) task.DueAt = request.DueAt; if (request.ParentTaskId.HasValue) { var parent = await taskRepo.GetByIdAsync(request.ParentTaskId.Value); diff --git a/TaskTracker.Tests/TaskTracker.Tests.csproj b/TaskTracker.Tests/TaskTracker.Tests.csproj index 1abed7b..84e3d05 100644 --- a/TaskTracker.Tests/TaskTracker.Tests.csproj +++ b/TaskTracker.Tests/TaskTracker.Tests.csproj @@ -7,6 +7,7 @@ + @@ -16,6 +17,8 @@ + + diff --git a/TaskTracker.Tests/TasksControllerDeadlineTests.cs b/TaskTracker.Tests/TasksControllerDeadlineTests.cs new file mode 100644 index 0000000..a160dd3 --- /dev/null +++ b/TaskTracker.Tests/TasksControllerDeadlineTests.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using TaskTracker.Api.Controllers; +using TaskTracker.Core.DTOs; +using TaskTracker.Core.Entities; +using TaskTracker.Infrastructure.Data; +using TaskTracker.Infrastructure.Repositories; +using Xunit; + +namespace TaskTracker.Tests; + +public class TasksControllerDeadlineTests +{ + [Fact] + public async Task Create_copies_due_at_including_offset_to_created_task() + { + await using var db = CreateDbContext(); + var controller = CreateController(db); + var dueAt = DateTimeOffset.Parse("2026-08-14T11:00:00-04:00"); + + var result = await controller.Create(new CreateTaskRequest { Title = "deadline", DueAt = dueAt }); + + var created = Assert.IsType(result); + var response = Assert.IsType>(created.Value); + Assert.Equal(dueAt, response.Data!.DueAt); + } + + [Fact] + public async Task Update_changes_due_at_when_value_is_supplied() + { + await using var db = CreateDbContext(); + var task = await AddTaskAsync(db, DateTimeOffset.Parse("2026-08-14T11:00:00-04:00")); + var controller = CreateController(db); + var dueAt = DateTimeOffset.Parse("2026-08-15T09:30:00+02:00"); + + var result = await controller.Update(task.Id, new UpdateTaskRequest { DueAt = dueAt }); + + var ok = Assert.IsType(result); + var response = Assert.IsType>(ok.Value); + Assert.Equal(dueAt, response.Data!.DueAt); + } + + [Fact] + public async Task Update_clears_due_at_only_when_clear_due_at_is_true() + { + await using var db = CreateDbContext(); + var task = await AddTaskAsync(db, DateTimeOffset.Parse("2026-08-14T11:00:00-04:00")); + var controller = CreateController(db); + + var result = await controller.Update(task.Id, new UpdateTaskRequest { ClearDueAt = true }); + + var ok = Assert.IsType(result); + var response = Assert.IsType>(ok.Value); + Assert.Null(response.Data!.DueAt); + } + + [Fact] + public async Task Update_with_omitted_due_at_preserves_existing_due_at() + { + await using var db = CreateDbContext(); + var dueAt = DateTimeOffset.Parse("2026-08-14T11:00:00-04:00"); + var task = await AddTaskAsync(db, dueAt); + var controller = CreateController(db); + + var result = await controller.Update(task.Id, new UpdateTaskRequest { Title = "renamed" }); + + var ok = Assert.IsType(result); + var response = Assert.IsType>(ok.Value); + Assert.Equal(dueAt, response.Data!.DueAt); + } + + private static TaskTrackerDbContext CreateDbContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new TaskTrackerDbContext(options); + } + + private static TasksController CreateController(TaskTrackerDbContext db) => + new(new TaskRepository(db), NullLogger.Instance); + + private static async Task AddTaskAsync(TaskTrackerDbContext db, DateTimeOffset dueAt) + { + var task = new WorkTask { Title = "existing", DueAt = dueAt }; + db.Tasks.Add(task); + await db.SaveChangesAsync(); + return task; + } +}