[FA-misc] Add ReportingService consumer unit tests

This commit is contained in:
gamer147
2026-01-30 16:47:26 -05:00
parent 2e4e2c26aa
commit 7c3df7ab11
3 changed files with 271 additions and 0 deletions

View File

@@ -0,0 +1,233 @@
using System.Text.Json;
using FictionArchive.Common.Enums;
using FictionArchive.Service.ReportingService.Consumers;
using FictionArchive.Service.ReportingService.Models;
using FictionArchive.Service.ReportingService.Services;
using FictionArchive.Service.Shared.Contracts.Events;
using FluentAssertions;
using MassTransit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using Xunit;
namespace FictionArchive.Service.ReportingService.Tests.Consumers;
public class JobStatusUpdateConsumerTests : IDisposable
{
private readonly ReportingDbContext _dbContext;
private readonly JobStatusUpdateConsumer _consumer;
public JobStatusUpdateConsumerTests()
{
var options = new DbContextOptionsBuilder<ReportingDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_dbContext = new TestReportingDbContext(options, NullLogger<ReportingDbContext>.Instance);
_consumer = new JobStatusUpdateConsumer(
NullLogger<JobStatusUpdateConsumer>.Instance,
_dbContext);
}
[Fact]
public async Task Should_create_new_job_on_first_event()
{
var jobId = Guid.NewGuid();
var context = CreateConsumeContext(new JobStatusUpdate(
jobId, null, "TestJob", "Test job display",
JobStatus.InProgress, null, new() { ["key1"] = "value1" }));
await _consumer.Consume(context);
var job = await _dbContext.Jobs.FindAsync(jobId);
job.Should().NotBeNull();
job!.JobType.Should().Be("TestJob");
job.DisplayName.Should().Be("Test job display");
job.Status.Should().Be(JobStatus.InProgress);
job.Metadata.Should().ContainKey("key1").WhoseValue.Should().Be("value1");
}
[Fact]
public async Task Should_update_status_on_subsequent_event()
{
var jobId = Guid.NewGuid();
// First event: create
await _consumer.Consume(CreateConsumeContext(new JobStatusUpdate(
jobId, null, "TestJob", "Test job",
JobStatus.InProgress, null, null)));
// Second event: update
await _consumer.Consume(CreateConsumeContext(new JobStatusUpdate(
jobId, null, "TestJob", "Test job",
JobStatus.Completed, null, null)));
var job = await _dbContext.Jobs.FindAsync(jobId);
job!.Status.Should().Be(JobStatus.Completed);
}
[Fact]
public async Task Should_merge_metadata_on_update()
{
var jobId = Guid.NewGuid();
// First event with initial metadata
await _consumer.Consume(CreateConsumeContext(new JobStatusUpdate(
jobId, null, "TestJob", "Test job",
JobStatus.InProgress, null, new() { ["NovelId"] = "42" })));
// Second event with additional metadata
await _consumer.Consume(CreateConsumeContext(new JobStatusUpdate(
jobId, null, "TestJob", "Test job",
JobStatus.Completed, null, new() { ["ChapterId"] = "7" })));
var job = await _dbContext.Jobs.FindAsync(jobId);
job!.Metadata.Should().ContainKey("NovelId").WhoseValue.Should().Be("42");
job.Metadata.Should().ContainKey("ChapterId").WhoseValue.Should().Be("7");
}
[Fact]
public async Task Should_not_overwrite_job_type_on_update()
{
var jobId = Guid.NewGuid();
await _consumer.Consume(CreateConsumeContext(new JobStatusUpdate(
jobId, null, "OriginalType", "Test job",
JobStatus.InProgress, null, null)));
await _consumer.Consume(CreateConsumeContext(new JobStatusUpdate(
jobId, null, "DifferentType", "Test job",
JobStatus.Completed, null, null)));
var job = await _dbContext.Jobs.FindAsync(jobId);
job!.JobType.Should().Be("OriginalType");
}
[Fact]
public async Task Should_not_overwrite_parent_job_id_on_update()
{
var jobId = Guid.NewGuid();
var parentId = Guid.NewGuid();
await _consumer.Consume(CreateConsumeContext(new JobStatusUpdate(
jobId, parentId, "TestJob", "Test job",
JobStatus.InProgress, null, null)));
await _consumer.Consume(CreateConsumeContext(new JobStatusUpdate(
jobId, null, "TestJob", "Test job",
JobStatus.Completed, null, null)));
var job = await _dbContext.Jobs.FindAsync(jobId);
job!.ParentJobId.Should().Be(parentId);
}
[Fact]
public async Task Should_set_error_message_on_failure()
{
var jobId = Guid.NewGuid();
await _consumer.Consume(CreateConsumeContext(new JobStatusUpdate(
jobId, null, "TestJob", "Test job",
JobStatus.Failed, "Something went wrong", null)));
var job = await _dbContext.Jobs.FindAsync(jobId);
job!.Status.Should().Be(JobStatus.Failed);
job.ErrorMessage.Should().Be("Something went wrong");
}
[Fact]
public async Task Should_store_parent_job_id()
{
var parentId = Guid.NewGuid();
var childId = Guid.NewGuid();
await _consumer.Consume(CreateConsumeContext(new JobStatusUpdate(
parentId, null, "ParentJob", "Parent",
JobStatus.InProgress, null, null)));
await _consumer.Consume(CreateConsumeContext(new JobStatusUpdate(
childId, parentId, "ChildJob", "Child",
JobStatus.InProgress, null, null)));
var child = await _dbContext.Jobs.FindAsync(childId);
child!.ParentJobId.Should().Be(parentId);
}
[Fact]
public async Task Should_handle_null_metadata_on_create()
{
var jobId = Guid.NewGuid();
await _consumer.Consume(CreateConsumeContext(new JobStatusUpdate(
jobId, null, "TestJob", "Test job",
JobStatus.InProgress, null, null)));
var job = await _dbContext.Jobs.FindAsync(jobId);
job!.Metadata.Should().BeNull();
}
[Fact]
public async Task Should_add_metadata_to_job_with_null_metadata()
{
var jobId = Guid.NewGuid();
// First event: no metadata
await _consumer.Consume(CreateConsumeContext(new JobStatusUpdate(
jobId, null, "TestJob", "Test job",
JobStatus.InProgress, null, null)));
// Second event: adds metadata
await _consumer.Consume(CreateConsumeContext(new JobStatusUpdate(
jobId, null, "TestJob", "Test job",
JobStatus.Completed, null, new() { ["result"] = "success" })));
var job = await _dbContext.Jobs.FindAsync(jobId);
job!.Metadata.Should().ContainKey("result").WhoseValue.Should().Be("success");
}
private static ConsumeContext<IJobStatusUpdate> CreateConsumeContext(JobStatusUpdate message)
{
var context = Substitute.For<ConsumeContext<IJobStatusUpdate>>();
context.Message.Returns(message);
return context;
}
public void Dispose()
{
_dbContext.Dispose();
}
/// <summary>
/// Test-specific subclass that adds a JSON value converter for Dictionary properties,
/// since the InMemory provider does not support the jsonb column type used in production.
/// </summary>
private class TestReportingDbContext : ReportingDbContext
{
public TestReportingDbContext(DbContextOptions options, ILogger<ReportingDbContext> logger)
: base(options, logger)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Job>(entity =>
{
entity.Property(j => j.Metadata)
.HasConversion(
v => v == null ? null : JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
v => v == null ? null : JsonSerializer.Deserialize<Dictionary<string, string>>(v, (JsonSerializerOptions?)null))
.HasColumnType(null!);
});
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Skip base OnConfiguring to avoid adding AuditInterceptor
// which is not needed for unit tests
}
}
}

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="MassTransit" Version="8.5.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.11" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NodaTime.Testing" Version="3.3.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FictionArchive.Service.ReportingService\FictionArchive.Service.ReportingService.csproj" />
</ItemGroup>
</Project>