test: cover list folders in the data and sidebar layers
Data: folder delete keeps its lists (ON DELETE SET NULL), rename patches name/icon without resetting sort order or collapse state. Sidebar: grouping on load, folder badge as the sum of its children, both move routes, and the persisted collapse state. Drops the LoadAsync/AddRowAsync normalization of a dangling folder_id: FK enforcement is on for every connection, so the row it guarded against cannot exist, and ContainerOf already falls back to the top level.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Data.Tests;
|
||||
|
||||
public sealed class ListFolderTests : IDisposable
|
||||
{
|
||||
private readonly string _dbPath;
|
||||
private readonly DbContextOptions<ClaudeDoDbContext> _options;
|
||||
|
||||
public ListFolderTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_folders_{Guid.NewGuid():N}.db");
|
||||
_options = new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
||||
.UseSqlite($"Data Source={_dbPath}")
|
||||
.Options;
|
||||
|
||||
using var ctx = new ClaudeDoDbContext(_options);
|
||||
ctx.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var suffix in new[] { "", "-wal", "-shm" })
|
||||
try { File.Delete(_dbPath + suffix); } catch { }
|
||||
}
|
||||
|
||||
private ClaudeDoDbContext Open() => new(_options);
|
||||
|
||||
private static ListFolderEntity NewFolder(string id, string name = "Group") =>
|
||||
new() { Id = id, Name = name, CreatedAt = DateTime.UtcNow };
|
||||
|
||||
[Fact]
|
||||
public async Task Deleting_a_folder_keeps_its_lists_and_moves_them_to_the_top_level()
|
||||
{
|
||||
await using (var ctx = Open())
|
||||
{
|
||||
var repo = new ListRepository(ctx);
|
||||
await repo.AddFolderAsync(NewFolder("f1"));
|
||||
await repo.AddAsync(new ListEntity { Id = "l1", Name = "Inside", CreatedAt = DateTime.UtcNow, FolderId = "f1" });
|
||||
}
|
||||
|
||||
await using (var ctx = Open())
|
||||
await new ListRepository(ctx).DeleteFolderAsync("f1");
|
||||
|
||||
await using (var ctx = Open())
|
||||
{
|
||||
var list = await ctx.Lists.AsNoTracking().FirstAsync(l => l.Id == "l1");
|
||||
Assert.Null(list.FolderId);
|
||||
Assert.Empty(await ctx.ListFolders.AsNoTracking().ToListAsync());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RenameFolderAsync_keeps_sort_order_and_collapse_state()
|
||||
{
|
||||
// The whole reason it patches instead of Update()-ing a rebuilt entity: a rename must not
|
||||
// silently unfold the folder or shuffle it to the top.
|
||||
await using (var ctx = Open())
|
||||
{
|
||||
var repo = new ListRepository(ctx);
|
||||
await repo.AddFolderAsync(NewFolder("f1", "First"));
|
||||
await repo.AddFolderAsync(NewFolder("f2", "Second"));
|
||||
await repo.SetFolderCollapsedAsync("f2", true);
|
||||
}
|
||||
|
||||
await using (var ctx = Open())
|
||||
await new ListRepository(ctx).RenameFolderAsync("f2", "Renamed", "Star");
|
||||
|
||||
await using (var ctx = Open())
|
||||
{
|
||||
var folder = await ctx.ListFolders.AsNoTracking().FirstAsync(f => f.Id == "f2");
|
||||
Assert.Equal("Renamed", folder.Name);
|
||||
Assert.Equal("Star", folder.Icon);
|
||||
Assert.True(folder.IsCollapsed);
|
||||
Assert.Equal(1, folder.SortOrder);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetListFolderAsync_moves_a_list_in_and_back_out()
|
||||
{
|
||||
await using (var ctx = Open())
|
||||
{
|
||||
var repo = new ListRepository(ctx);
|
||||
await repo.AddFolderAsync(NewFolder("f1"));
|
||||
await repo.AddAsync(new ListEntity { Id = "l1", Name = "Roaming", CreatedAt = DateTime.UtcNow });
|
||||
}
|
||||
|
||||
await using (var ctx = Open())
|
||||
await new ListRepository(ctx).SetListFolderAsync("l1", "f1");
|
||||
|
||||
await using (var ctx = Open())
|
||||
Assert.Equal("f1", (await ctx.Lists.AsNoTracking().FirstAsync(l => l.Id == "l1")).FolderId);
|
||||
|
||||
await using (var ctx = Open())
|
||||
await new ListRepository(ctx).SetListFolderAsync("l1", null);
|
||||
|
||||
await using (var ctx = Open())
|
||||
Assert.Null((await ctx.Lists.AsNoTracking().FirstAsync(l => l.Id == "l1")).FolderId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddFolderAsync_appends_in_creation_order()
|
||||
{
|
||||
await using var ctx = Open();
|
||||
var repo = new ListRepository(ctx);
|
||||
await repo.AddFolderAsync(NewFolder("f1", "One"));
|
||||
await repo.AddFolderAsync(NewFolder("f2", "Two"));
|
||||
|
||||
var folders = await repo.GetFoldersAsync();
|
||||
Assert.Equal(["One", "Two"], folders.Select(f => f.Name));
|
||||
Assert.Equal([0, 1], folders.Select(f => f.SortOrder));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user