234 lines
8.1 KiB
C#
234 lines
8.1 KiB
C#
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
|
|
}
|
|
}
|
|
}
|