Files
novelly/tests/Novelly.Api.Tests/ActivityTests.cs
T
James Wampler 71953220aa
CI / build-and-push (push) Successful in 52s
CI / deploy (push) Successful in 9s
Add GitHub-style activity contribution graph
Record create/update/delete events across novel content (chapters, beats,
characters, arc stages, tags, locations, questions) into an append-only
ActivityEvent log, aggregate by UTC day, and surface as a heatmap on the
novels list and each novel's dashboard. Backfills history from existing
CreatedAt timestamps on first boot after the migration.
2026-08-19 17:39:13 -07:00

128 lines
4.5 KiB
C#

using Microsoft.EntityFrameworkCore;
using Novelly.Api.Activity;
using Novelly.Api.Beats;
using Novelly.Api.Chapters;
using Novelly.Api.Novels;
using Novelly.Api.Users;
namespace Novelly.Api.Tests;
[TestFixture]
public class ActivityTests : ServiceTestFixture
{
private Guid AsNewUser(GlobalRole globalRole)
{
var user = new NovellyUser
{
Id = Guid.NewGuid(),
UserName = $"{Guid.NewGuid()}@novelly.test",
Email = $"{Guid.NewGuid()}@novelly.test",
DisplayName = "Test User",
GlobalRole = globalRole
};
Db.Context.Users.Add(user);
Db.Context.SaveChanges();
UserContext.UserId = user.Id;
UserContext.GlobalRole = globalRole;
return user.Id;
}
[Test]
public async Task Writing_prose_records_the_word_delta_for_that_day()
{
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
var chapter = await Chapters.CreateAsync(novel.Id, new CreateChapterRequest("Landfall"));
await Chapters.UpdateAsync(chapter!.Id, new UpdateChapterRequest(Prose: "one two three four five"));
var calendar = await Activity.GetForNovelAsync(novel.Id, days: 7);
Assert.That(calendar.Days.Sum(d => d.Words), Is.EqualTo(5));
}
[Test]
public async Task Editing_a_beat_records_one_edit_not_two()
{
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
var chapter = await Chapters.CreateAsync(novel.Id, new CreateChapterRequest("Landfall"));
var beat = await Beats.CreateAsync(chapter!.Id, new CreateBeatRequest("She finds the map"));
await Beats.UpdateAsync(beat!.Id, new UpdateBeatRequest(WhatHappened: "She finds the map under the floorboards."));
var beatEvents = await Db.Context.ActivityEvents.CountAsync(e => e.EntityKind == ActivityEntityKind.Beat);
var chapterEvents = await Db.Context.ActivityEvents.CountAsync(e => e.EntityKind == ActivityEntityKind.Chapter);
Assert.Multiple(() =>
{
Assert.That(beatEvents, Is.EqualTo(2));
Assert.That(chapterEvents, Is.EqualTo(1));
});
}
[Test]
public async Task Deleting_a_chapter_records_a_negative_word_delta()
{
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
var chapter = await Chapters.CreateAsync(novel.Id, new CreateChapterRequest("Landfall", Prose: "one two three"));
await Chapters.DeleteAsync(chapter!.Id);
var calendar = await Activity.GetForNovelAsync(novel.Id, days: 7);
Assert.That(calendar.Days.Sum(d => d.Words), Is.EqualTo(0));
}
[Test]
public async Task The_calendar_groups_multiple_edits_into_one_day()
{
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
var chapter = await Chapters.CreateAsync(novel.Id, new CreateChapterRequest("Landfall"));
await Chapters.UpdateAsync(chapter!.Id, new UpdateChapterRequest(Summary: "First pass"));
await Chapters.UpdateAsync(chapter.Id, new UpdateChapterRequest(Summary: "Second pass"));
var calendar = await Activity.GetForNovelAsync(novel.Id, days: 7);
Assert.Multiple(() =>
{
Assert.That(calendar.Days, Has.Count.EqualTo(1));
Assert.That(calendar.Days[0].Edits, Is.EqualTo(4));
});
}
[Test]
public async Task The_calendar_omits_days_with_no_activity()
{
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
var calendar = await Activity.GetForNovelAsync(novel.Id, days: 7);
Assert.That(calendar.Days, Has.Count.EqualTo(1));
}
[Test]
public async Task The_calendar_excludes_novels_the_user_cannot_read()
{
var novel = await Novels.CreateAsync(new CreateNovelRequest("Someone Else's Novel"));
AsNewUser(GlobalRole.Writer);
await Novels.CreateAsync(new CreateNovelRequest("My Own Novel"));
var calendar = await Activity.GetForCurrentUserAsync(days: 7);
Assert.That(calendar.TotalEdits, Is.EqualTo(1));
}
[Test]
public async Task Deleting_a_novel_removes_its_activity_events()
{
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
await Chapters.CreateAsync(novel.Id, new CreateChapterRequest("Landfall"));
await Novels.DeleteAsync(novel.Id);
Assert.That(await Db.Context.ActivityEvents.CountAsync(e => e.NovelId == novel.Id), Is.EqualTo(0));
}
}