Files
ClaudeDo/tests/ClaudeDo.Data.Tests/TaskAttachmentRepositoryTests.cs
Mika Kuns 3f9f047955 feat(attachments): data layer for task file attachments
TaskAttachmentEntity (+config, cascade FK), TaskAttachmentRepository, and an
AttachmentStore that writes files under ~/.todo-app/attachments/<taskId>/ with
a path-traversal guard and a 5 MB cap. TaskPromptComposer gains an optional
read-only 'Reference files' section. Migration AddTaskAttachments.
2026-06-26 16:11:48 +02:00

117 lines
3.7 KiB
C#

using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Data.Tests;
public sealed class TaskAttachmentRepositoryTests : IDisposable
{
private readonly string _dbPath;
private readonly ClaudeDoDbContext _ctx;
private readonly TaskAttachmentRepository _repo;
private const string ListId = "l1";
private const string TaskId = "t1";
public TaskAttachmentRepositoryTests()
{
_dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_att_{Guid.NewGuid():N}.db");
var options = new DbContextOptionsBuilder<ClaudeDoDbContext>()
.UseSqlite($"Data Source={_dbPath}")
.Options;
_ctx = new ClaudeDoDbContext(options);
_ctx.Database.EnsureCreated();
_repo = new TaskAttachmentRepository(_ctx);
_ctx.Lists.Add(new ListEntity { Id = ListId, Name = "Test", CreatedAt = DateTime.UtcNow });
_ctx.Tasks.Add(new TaskEntity { Id = TaskId, ListId = ListId, Title = "T", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow });
_ctx.SaveChanges();
}
public void Dispose()
{
_ctx.Dispose();
foreach (var suffix in new[] { "", "-wal", "-shm" })
try { File.Delete(_dbPath + suffix); } catch { }
}
private TaskAttachmentEntity MakeAttachment(string fileName) => new()
{
Id = Guid.NewGuid().ToString(),
TaskId = TaskId,
FileName = fileName,
ByteSize = 100,
CreatedAt = DateTime.UtcNow,
};
[Fact]
public async Task Add_ListByTaskId_Delete_roundtrip()
{
var a1 = MakeAttachment("file1.txt");
var a2 = MakeAttachment("file2.txt");
await _repo.AddAsync(a1);
await _repo.AddAsync(a2);
var list = await _repo.ListByTaskIdAsync(TaskId);
Assert.Equal(2, list.Count);
Assert.Contains(list, a => a.FileName == "file1.txt");
Assert.Contains(list, a => a.FileName == "file2.txt");
await _repo.DeleteAsync(TaskId, "file1.txt");
var afterDelete = await _repo.ListByTaskIdAsync(TaskId);
Assert.Single(afterDelete);
Assert.Equal("file2.txt", afterDelete[0].FileName);
}
[Fact]
public async Task GetAsync_returns_correct_entity()
{
var a = MakeAttachment("target.txt");
await _repo.AddAsync(a);
var found = await _repo.GetAsync(TaskId, "target.txt");
Assert.NotNull(found);
Assert.Equal(a.Id, found!.Id);
}
[Fact]
public async Task GetAsync_returns_null_when_missing()
{
var result = await _repo.GetAsync(TaskId, "nope.txt");
Assert.Null(result);
}
[Fact]
public async Task DeleteAllForTask_clears_all_rows_for_task()
{
await _repo.AddAsync(MakeAttachment("a.txt"));
await _repo.AddAsync(MakeAttachment("b.txt"));
await _repo.AddAsync(MakeAttachment("c.txt"));
await _repo.DeleteAllForTaskAsync(TaskId);
var list = await _repo.ListByTaskIdAsync(TaskId);
Assert.Empty(list);
}
[Fact]
public async Task ListByTaskId_ordered_by_created_at()
{
var base_ = DateTime.UtcNow;
var a1 = new TaskAttachmentEntity { Id = Guid.NewGuid().ToString(), TaskId = TaskId, FileName = "first.txt", ByteSize = 1, CreatedAt = base_ };
var a2 = new TaskAttachmentEntity { Id = Guid.NewGuid().ToString(), TaskId = TaskId, FileName = "second.txt", ByteSize = 1, CreatedAt = base_.AddSeconds(1) };
await _repo.AddAsync(a2);
await _repo.AddAsync(a1);
var list = await _repo.ListByTaskIdAsync(TaskId);
Assert.Equal("first.txt", list[0].FileName);
Assert.Equal("second.txt", list[1].FileName);
}
}