feat: added Sdn repo and added some initial tests

This commit is contained in:
Stevan Freeborn
2024-08-21 09:57:24 -05:00
parent 00994cd4d9
commit ecc26d86f6
18 changed files with 474 additions and 373 deletions
+2 -1
View File
@@ -4,6 +4,7 @@
"cSpell.words": [
"OFAC",
"Sdns",
"Szalay"
"Szalay",
"upserting"
]
}
@@ -1,5 +1,3 @@
using Bogus;
namespace SanctionsSearch.Worker.Tests.Fakes;
class SdnFaker : Faker<Sdn>
@@ -1,77 +0,0 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using SanctionsSearch.Worker.Tests.Fakes;
namespace SanctionsSearch.Worker.Tests.Integration;
public class EfRepositoryTests : IAsyncLifetime
{
private readonly AppDbContext _context;
private readonly EfRepository<Sdn> _sdnRepository;
private readonly SdnFaker _sdnFaker = new();
public EfRepositoryTests()
{
var loggerFactory = LoggerFactory.Create(builder => builder.ClearProviders());
_context = new AppDbContext(new DbOptions { DatabaseName = $"{Guid.NewGuid()}.db" });
_sdnRepository = new EfRepository<Sdn>(_context, loggerFactory.CreateLogger<EfRepository<Sdn>>());
}
[Fact]
public async Task Upsert_WithNewEntity_ShouldAddEntityToDatabase()
{
var sdn = _sdnFaker.Generate();
await _sdnRepository.Upsert(sdn);
await _context.SaveChangesAsync();
var result = await _context.Sdns.FindAsync(sdn.Id);
result.Should().BeEquivalentTo(sdn);
result!.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
result.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
}
[Fact]
public async Task Upsert_WithExistingEntity_ShouldUpdateEntityInDatabase()
{
var sdn = _sdnFaker.Generate();
await _sdnRepository.Upsert(sdn);
await _context.SaveChangesAsync();
var createdSdn = await _context.Sdns.FindAsync(sdn.Id);
createdSdn.Should().BeEquivalentTo(sdn);
createdSdn!.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
createdSdn.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
var updatedSdn = _sdnFaker.Generate();
updatedSdn.Id = sdn.Id;
await _sdnRepository.Upsert(updatedSdn);
await _context.SaveChangesAsync();
var result = await _context.Sdns.FindAsync(sdn.Id);
result.Should().BeEquivalentTo(updatedSdn);
result!.CreatedAt.Should().BeSameDateAs(createdSdn.CreatedAt);
result.UpdatedAt.Should().BeAfter(createdSdn.UpdatedAt);
}
public async Task InitializeAsync()
{
await _context.Database.MigrateAsync();
}
public async Task DisposeAsync()
{
await _context.DisposeAsync();
SqliteConnection.ClearAllPools();
File.Delete(_context.DatabasePath);
}
}
@@ -0,0 +1,104 @@
namespace SanctionsSearch.Worker.Tests.Integration;
public class SdnRepositoryTests : IAsyncLifetime
{
private readonly AppDbContext _context;
private readonly SdnRepository _sdnRepository;
private readonly SdnFaker _sdnFaker = new();
private readonly Mock<TimeProvider> _timeProvider = new();
public SdnRepositoryTests()
{
var loggerFactory = LoggerFactory.Create(builder => builder.ClearProviders());
var options = new DbOptions { DatabaseName = $"{Guid.NewGuid()}.db" };
_context = new AppDbContext(options, _timeProvider.Object);
_sdnRepository = new SdnRepository(_context, new Logger<SdnRepository>(loggerFactory));
}
[Fact]
public async Task Upsert_WithNewEntity_ShouldAddEntityToDatabase()
{
var now = DateTimeOffset.UtcNow;
_timeProvider.Setup(x => x.GetUtcNow()).Returns(now);
var sdn = _sdnFaker.Generate();
await _sdnRepository.Upsert(sdn);
await _context.SaveChangesAsync();
var result = await _context.Sdns.FindAsync(sdn.Id);
result.Should().BeEquivalentTo(sdn);
result!.CreatedAt.Should().Be(now.DateTime);
result.UpdatedAt.Should().Be(now.DateTime);
}
[Fact]
public async Task Upsert_WithExistingEntity_ShouldUpdateEntityInDatabase()
{
var createdTimeStamp = DateTimeOffset.UtcNow;
var updatedTimeStamp = createdTimeStamp.AddSeconds(2);
_timeProvider.Setup(x => x.GetUtcNow()).Returns(createdTimeStamp);
var sdn = _sdnFaker.Generate();
// Insert the entity
await _sdnRepository.Upsert(sdn);
await _context.SaveChangesAsync();
// Assert the entity was created
var createdSdn = await _context.Sdns.FindAsync(sdn.Id);
createdSdn.Should().BeEquivalentTo(sdn);
createdSdn!.CreatedAt.Should().Be(createdTimeStamp.DateTime);
createdSdn.UpdatedAt.Should().Be(createdTimeStamp.DateTime);
_timeProvider.Setup(x => x.GetUtcNow()).Returns(updatedTimeStamp);
// Update the entity
sdn.Name = "Updated";
await _sdnRepository.Upsert(sdn);
await _context.SaveChangesAsync();
// Assert the entity was updated
var result = await _context.Sdns.FindAsync(sdn.Id);
result.Should().BeEquivalentTo(sdn);
result!.CreatedAt.Should().BeSameDateAs(createdTimeStamp.DateTime);
result.UpdatedAt.Should().BeSameDateAs(updatedTimeStamp.DateTime);
}
[Fact]
public async Task Find_WithPredicate_ShouldReturnEntitiesMatchingPredicate()
{
var sdn1 = _sdnFaker.Generate();
var sdn2 = _sdnFaker.Generate();
var sdn3 = _sdnFaker.Generate();
await _sdnRepository.Upsert(sdn1);
await _sdnRepository.Upsert(sdn2);
await _sdnRepository.Upsert(sdn3);
await _context.SaveChangesAsync();
var result = await _sdnRepository.Find(x => x.Id == sdn2.Id);
result.Should().HaveCount(1);
result.First().Should().BeEquivalentTo(sdn2);
}
public async Task InitializeAsync()
{
await _context.Database.MigrateAsync();
}
public async Task DisposeAsync()
{
await _context.DisposeAsync();
SqliteConnection.ClearAllPools();
File.Delete(_context.DatabasePath);
}
}
@@ -31,6 +31,7 @@
<CoverletOutput>./TestResults/coverage/</CoverletOutput>
<CoverletOutputFormat>cobertura</CoverletOutputFormat>
<Include>[SanctionsSearch.Worker]*</Include>
<Exclude>[SanctionsSearch.Worker]SanctionsSearch.Worker.Migrations*</Exclude>
</PropertyGroup>
<Target Name="GenerateHtmlCoverageReport" AfterTargets="GenerateCoverageResultAfterTest">
@@ -1,6 +1,3 @@
using System.Net;
using System.Text;
namespace SanctionsSearch.Worker.Tests.Unit;
public class OfacFileServiceTests
@@ -0,0 +1,63 @@
namespace SanctionsSearch.Worker.Tests.Unit;
public class SdnRepositoryTests
{
private readonly Mock<DbContext> _context = new();
private readonly Mock<ILogger<SdnRepository>> _logger = new();
private readonly SdnFaker _sdnFaker = new();
private readonly SdnRepository _sdnRepository;
public SdnRepositoryTests()
{
_sdnRepository = new SdnRepository(_context.Object, _logger.Object);
}
[Fact]
public async Task Upsert_WhenExceptionIsThrown_ItShouldLogError()
{
var sdn = _sdnFaker.Generate();
var mockSet = new Mock<DbSet<Sdn>>();
mockSet
.Setup(x => x.FindAsync(sdn.Id))
.Throws<Exception>();
_context
.Setup(x => x.Set<Sdn>())
.Returns(mockSet.Object);
await _sdnRepository.Upsert(sdn);
_logger.Verify(
x => x.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
)
);
}
[Fact]
public async Task Find_WhenExceptionIsThrown_ItShouldReturnEmptyListAndLogError()
{
_context
.Setup(x => x.Set<Sdn>())
.Throws<Exception>();
var result = await _sdnRepository.Find(x => x.Id == 1);
result.Should().BeEmpty();
_logger.Verify(
x => x.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
)
);
}
}
+10 -2
View File
@@ -1,5 +1,12 @@
global using System.Net;
global using System.Text;
global using Bogus;
global using FluentAssertions;
global using Microsoft.Data.Sqlite;
global using Microsoft.EntityFrameworkCore;
global using Microsoft.Extensions.Logging;
global using Microsoft.Extensions.Options;
@@ -7,7 +14,8 @@ global using Moq;
global using RichardSzalay.MockHttp;
global using SanctionsSearch.Worker.Services;
global using SanctionsSearch.Worker.Options;
global using SanctionsSearch.Worker.Models;
global using SanctionsSearch.Worker.Options;
global using SanctionsSearch.Worker.Persistence;
global using SanctionsSearch.Worker.Services;
global using SanctionsSearch.Worker.Tests.Fakes;
@@ -1,5 +1,3 @@
using FluentResults;
namespace SanctionsSearch.Worker.Interfaces;
interface IOfacFileService
@@ -1,4 +1,5 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
@@ -1,8 +1,9 @@
namespace SanctionsSearch.Worker.Persistence;
class AppDbContext(DbOptions options) : DbContext
class AppDbContext(DbOptions options, TimeProvider timeProvider) : DbContext
{
private readonly DbOptions _options = options;
private readonly TimeProvider _timeProvider = timeProvider;
private SqliteConnection? _connection;
internal string DatabasePath { get; private set; } = string.Empty;
public DbSet<Sdn> Sdns { get; set; } = default!;
@@ -32,8 +33,7 @@ class AppDbContext(DbOptions options) : DbContext
public async override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
// TODO: Use TimeProvider instead of DateTime.UtcNow
var now = DateTime.UtcNow;
var now = _timeProvider.GetUtcNow().DateTime;
foreach (var changedEntity in ChangeTracker.Entries())
{
@@ -2,10 +2,10 @@ namespace SanctionsSearch.Worker.Persistence;
class EfRepository<T> : IRepository<T> where T : Entity
{
protected readonly AppDbContext _context;
protected readonly DbContext _context;
protected readonly ILogger<EfRepository<T>> _logger;
public EfRepository(AppDbContext context, ILogger<EfRepository<T>> logger)
public EfRepository(DbContext context, ILogger<EfRepository<T>> logger)
{
_context = context;
_logger = logger;
@@ -1,7 +1,7 @@
namespace SanctionsSearch.Worker.Persistence;
class SdnRepository(
AppDbContext dbContext,
DbContext dbContext,
ILogger<SdnRepository> logger
) : EfRepository<Sdn>(dbContext, logger), ISdnRepository
{
+27 -18
View File
@@ -1,11 +1,10 @@
using SanctionsSearch.Worker.Persistence;
using SanctionsSearch.Worker.Setup;
namespace SanctionsSearch.Worker;
if (EF.IsDesignTime)
class Program
{
Host.CreateDefaultBuilder().Build().Run();
return;
}
async static Task Main(string[] args)
{
if (EF.IsDesignTime) return;
Log.Logger = new LoggerConfiguration()
.Enrich.WithProperty("Application", "SanctionsSearch.Worker")
@@ -24,17 +23,7 @@ try
{
Log.Information("Starting Sanctions Search worker");
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddSerilog();
builder.Services.ConfigureOptions<OfacFileServiceOptionsSetup>();
builder.Services.ConfigureOptions<DbOptionsSetup>();
builder.Services.AddScoped(rs => rs.GetRequiredService<IOptionsSnapshot<DbOptions>>().Value);
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddHostedService<Worker>();
var builder = CreateHostBuilder(args);
var host = builder.Build();
@@ -48,7 +37,7 @@ try
await context.Database.MigrateAsync();
}
host.Run();
await host.RunAsync();
Log.Information("Stopping Sanctions Search worker");
}
@@ -60,3 +49,23 @@ finally
{
await Log.CloseAndFlushAsync();
}
}
static HostApplicationBuilder CreateHostBuilder(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddSerilog();
builder.Services.ConfigureOptions<OfacFileServiceOptionsSetup>();
builder.Services.ConfigureOptions<DbOptionsSetup>();
builder.Services.AddScoped(rs => rs.GetRequiredService<IOptionsSnapshot<DbOptions>>().Value);
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddHostedService<Worker>();
return builder;
}
}
@@ -1,6 +1,3 @@
using FluentResults;
namespace SanctionsSearch.Worker.Services;
class OfacFileService(
+11 -10
View File
@@ -1,18 +1,19 @@
global using System.Linq.Expressions;
global using System.Reflection;
global using FluentResults;
global using Microsoft.Data.Sqlite;
global using Microsoft.EntityFrameworkCore;
global using Microsoft.Extensions.Options;
global using SanctionsSearch.Worker.Interfaces;
global using SanctionsSearch.Worker.Options;
global using SanctionsSearch.Worker;
global using SanctionsSearch.Worker.Models;
global using SanctionsSearch.Worker.Options;
global using SanctionsSearch.Worker.Persistence;
global using SanctionsSearch.Worker.Services;
global using SanctionsSearch.Worker.Setup;
global using Serilog;
global using Serilog.Exceptions;
global using Serilog.Formatting.Compact;
global using Microsoft.EntityFrameworkCore;
global using System.Linq.Expressions;
global using System.Reflection;
global using Microsoft.Data.Sqlite;