Files
ClaudeDo/tests/ClaudeDo.Data.Tests/ListFolderTests.cs
T

219 lines
8.2 KiB
C#

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", "#7FA7C9");
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.Equal("#7FA7C9", folder.Color);
Assert.True(folder.IsCollapsed);
Assert.Equal(1, folder.SortOrder);
}
}
[Fact]
public async Task List_color_round_trips_and_defaults_to_null()
{
await using (var ctx = Open())
{
var repo = new ListRepository(ctx);
await repo.AddAsync(new ListEntity { Id = "l1", Name = "Plain", CreatedAt = DateTime.UtcNow });
await repo.AddAsync(new ListEntity { Id = "l2", Name = "Tinted", CreatedAt = DateTime.UtcNow, Color = "Swatch3" });
}
await using (var ctx = Open())
{
var repo = new ListRepository(ctx);
Assert.Null((await ctx.Lists.AsNoTracking().FirstAsync(l => l.Id == "l1")).Color);
Assert.Equal("Swatch3", (await ctx.Lists.AsNoTracking().FirstAsync(l => l.Id == "l2")).Color);
var tinted = (await repo.GetByIdAsync("l2"))!;
tinted.Color = "#A1B2C3";
await repo.UpdateAsync(tinted);
}
await using (var ctx = Open())
Assert.Equal("#A1B2C3", (await ctx.Lists.AsNoTracking().FirstAsync(l => l.Id == "l2")).Color);
}
[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 ReorderSidebarAsync_numbers_folders_and_lists_in_one_sequence()
{
await using var ctx = Open();
var repo = new ListRepository(ctx);
await repo.AddFolderAsync(NewFolder("f1"));
await repo.AddAsync(new ListEntity { Id = "inside", Name = "Inside", CreatedAt = DateTime.UtcNow, FolderId = "f1" });
await repo.AddAsync(new ListEntity { Id = "top", Name = "Top", CreatedAt = DateTime.UtcNow });
// Sidebar: the top-level list above the folder, whose one child follows the folder.
await repo.ReorderSidebarAsync([("top", false), ("f1", true), ("inside", false)]);
Assert.Equal(0, (await repo.GetByIdAsync("top"))!.SortOrder);
Assert.Equal(1, (await repo.GetFoldersAsync()).Single().SortOrder);
Assert.Equal(2, (await repo.GetByIdAsync("inside"))!.SortOrder);
}
[Fact]
public async Task AddAsync_appends_past_the_folders_too()
{
await using var ctx = Open();
var repo = new ListRepository(ctx);
await repo.AddFolderAsync(NewFolder("f1"));
await repo.AddFolderAsync(NewFolder("f2"));
await repo.AddAsync(new ListEntity { Id = "l1", Name = "Fresh", CreatedAt = DateTime.UtcNow });
// 2, not 0 — a new list must not jump above the existing folders on the next load.
Assert.Equal(2, (await repo.GetByIdAsync("l1"))!.SortOrder);
}
[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));
}
[Fact]
public async Task SetFolderParentAsync_nests_a_folder_and_moves_it_back_out()
{
await using (var ctx = Open())
{
var repo = new ListRepository(ctx);
await repo.AddFolderAsync(NewFolder("parent"));
await repo.AddFolderAsync(NewFolder("child"));
}
await using (var ctx = Open())
await new ListRepository(ctx).SetFolderParentAsync("child", "parent");
await using (var ctx = Open())
Assert.Equal("parent", (await ctx.ListFolders.AsNoTracking().FirstAsync(f => f.Id == "child")).ParentFolderId);
await using (var ctx = Open())
await new ListRepository(ctx).SetFolderParentAsync("child", null);
await using (var ctx = Open())
Assert.Null((await ctx.ListFolders.AsNoTracking().FirstAsync(f => f.Id == "child")).ParentFolderId);
}
[Fact]
public async Task Deleting_a_folder_keeps_its_subfolder_and_moves_it_to_the_top_level()
{
await using (var ctx = Open())
{
var repo = new ListRepository(ctx);
await repo.AddFolderAsync(NewFolder("parent"));
await repo.AddFolderAsync(NewFolder("child"));
await repo.SetFolderParentAsync("child", "parent");
}
await using (var ctx = Open())
await new ListRepository(ctx).DeleteFolderAsync("parent");
await using (var ctx = Open())
{
var child = await ctx.ListFolders.AsNoTracking().SingleAsync();
Assert.Equal("child", child.Id);
Assert.Null(child.ParentFolderId);
}
}
}