[FA-misc] Add ReportingService consumer unit tests
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.Service.User
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.Service.ReportingService", "FictionArchive.Service.ReportingService\FictionArchive.Service.ReportingService.csproj", "{F29F7969-2B40-4B92-A8F5-9544A4F700DC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.Service.ReportingService.Tests", "FictionArchive.Service.ReportingService.Tests\FictionArchive.Service.ReportingService.Tests.csproj", "{E704ACF1-2E1D-4A1C-BBCE-8FAE9F1A9944}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -79,5 +81,9 @@ Global
|
||||
{F29F7969-2B40-4B92-A8F5-9544A4F700DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F29F7969-2B40-4B92-A8F5-9544A4F700DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F29F7969-2B40-4B92-A8F5-9544A4F700DC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E704ACF1-2E1D-4A1C-BBCE-8FAE9F1A9944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E704ACF1-2E1D-4A1C-BBCE-8FAE9F1A9944}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E704ACF1-2E1D-4A1C-BBCE-8FAE9F1A9944}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E704ACF1-2E1D-4A1C-BBCE-8FAE9F1A9944}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
Reference in New Issue
Block a user