Adds SoftDelete/Trash across characters, chapters, locations, beats with a purge schedule and Trash page. Reworks the web client for keyboard-driven navigation (focus helpers, help overlay, keyboard.md doc). Moves the ChapterPage tag editor to the bottom of the page to match CharacterDetailPage.
70 lines
2.6 KiB
C#
70 lines
2.6 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Novelly.Api.Chapters;
|
|
using Novelly.Api.Novels;
|
|
using Novelly.Api.Trash;
|
|
|
|
namespace Novelly.Api.Tests;
|
|
|
|
[TestFixture]
|
|
public class TrashPurgeScheduleTests
|
|
{
|
|
[Test]
|
|
public void Before_the_run_time_the_next_run_is_today() =>
|
|
Assert.That(
|
|
TrashPurgeSchedule.NextRunAfter(new DateTimeOffset(2026, 8, 20, 1, 0, 0, TimeSpan.Zero), new TimeOnly(2, 0)),
|
|
Is.EqualTo(new DateTimeOffset(2026, 8, 20, 2, 0, 0, TimeSpan.Zero)));
|
|
|
|
[Test]
|
|
public void After_the_run_time_the_next_run_is_tomorrow() =>
|
|
Assert.That(
|
|
TrashPurgeSchedule.NextRunAfter(new DateTimeOffset(2026, 8, 20, 3, 0, 0, TimeSpan.Zero), new TimeOnly(2, 0)),
|
|
Is.EqualTo(new DateTimeOffset(2026, 8, 21, 2, 0, 0, TimeSpan.Zero)));
|
|
|
|
[Test]
|
|
public void Exactly_at_the_run_time_the_next_run_is_tomorrow() =>
|
|
Assert.That(
|
|
TrashPurgeSchedule.NextRunAfter(new DateTimeOffset(2026, 8, 20, 2, 0, 0, TimeSpan.Zero), new TimeOnly(2, 0)),
|
|
Is.EqualTo(new DateTimeOffset(2026, 8, 21, 2, 0, 0, TimeSpan.Zero)));
|
|
}
|
|
|
|
[TestFixture]
|
|
public class TrashPurgeSweepTests : ServiceTestFixture
|
|
{
|
|
private Guid _novelId;
|
|
|
|
protected override void OnSetUp() =>
|
|
_novelId = Novels.CreateAsync(new CreateNovelRequest("The Salt Road")).Result.Id;
|
|
|
|
[Test]
|
|
public async Task A_sweep_removes_only_items_older_than_the_retention_window()
|
|
{
|
|
var old = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Old News"));
|
|
var recent = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Fresh Off The Press"));
|
|
|
|
await Chapters.DeleteAsync(old!.Id);
|
|
await Chapters.DeleteAsync(recent!.Id);
|
|
|
|
using (var context = Db.CreateContext())
|
|
{
|
|
var oldRow = await context.Chapters.IgnoreQueryFilters().SingleAsync(c => c.Id == old.Id);
|
|
oldRow.DeletedAt = DateTimeOffset.UtcNow.AddDays(-31);
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
var cutoff = DateTimeOffset.UtcNow.AddDays(-30);
|
|
var (characters, chapters, locations) = await TrashPurgeRunner.SweepAsync(Db.Context, cutoff, CancellationToken.None);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That((characters, chapters, locations), Is.EqualTo((0, 1, 0)));
|
|
});
|
|
|
|
using var verification = Db.CreateContext();
|
|
Assert.Multiple(async () =>
|
|
{
|
|
Assert.That(await verification.Chapters.IgnoreQueryFilters().AnyAsync(c => c.Id == old.Id), Is.False);
|
|
Assert.That(await verification.Chapters.IgnoreQueryFilters().AnyAsync(c => c.Id == recent.Id), Is.True);
|
|
});
|
|
}
|
|
}
|