diff --git a/Directory.Build.props b/Directory.Build.props
deleted file mode 100644
index 8702b6e..0000000
--- a/Directory.Build.props
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
- $(WarningsNotAsErrors);
- AL0025;AL0026;AL0039;AL0070;AL0081;AL0101;AL0114;AL0137;
- RS0030;
- CA1002;CA1032;CA1034;CA1052;CA1056;CA1307;CA1725;CA1819;CA1822;CA1823;CA1852;CA1859;
- CA2000;CA2012;CA2201;CA5394;
- IDE0370;IDE1006
-
-
-
-
diff --git a/PaperlessREST.Tests/DocumentBuilder.cs b/PaperlessREST.Tests/DocumentBuilder.cs
index a153593..e25387a 100644
--- a/PaperlessREST.Tests/DocumentBuilder.cs
+++ b/PaperlessREST.Tests/DocumentBuilder.cs
@@ -6,7 +6,7 @@ public sealed class DocumentBuilder
private const string DefaultStoragePathFormat = "documents/{0:yyyy-MM}/{1}.pdf";
private const string DefaultExtractedContent = "Extracted content";
private string? _content;
- private DateTimeOffset _createdAt = DateTimeOffset.UtcNow;
+ private DateTimeOffset _createdAt = TimeProvider.System.GetUtcNow();
private string _fileName = DefaultFileName;
private Guid _id = Guid.CreateVersion7();
@@ -68,7 +68,7 @@ public DocumentBuilder WithProcessedAt(DateTimeOffset? processedAt)
public DocumentBuilder WithSummary(string? summary, DateTimeOffset? generatedAt = null)
{
_summary = summary;
- _summaryGeneratedAt = generatedAt ?? (_summary is not null ? DateTimeOffset.UtcNow : null);
+ _summaryGeneratedAt = generatedAt ?? (_summary is not null ? TimeProvider.System.GetUtcNow() : null);
return this;
}
@@ -84,7 +84,7 @@ public DocumentBuilder AsCompleted(string content = DefaultExtractedContent)
{
_status = DocumentStatus.Completed;
_content = content;
- _processedAt = DateTimeOffset.UtcNow;
+ _processedAt = TimeProvider.System.GetUtcNow();
return this;
}
@@ -92,7 +92,7 @@ public DocumentBuilder AsFailed()
{
_status = DocumentStatus.Failed;
_content = null;
- _processedAt = DateTimeOffset.UtcNow;
+ _processedAt = TimeProvider.System.GetUtcNow();
return this;
}
diff --git a/PaperlessREST.Tests/GlobalUsings.cs b/PaperlessREST.Tests/GlobalUsings.cs
index 13e9a53..474d20f 100644
--- a/PaperlessREST.Tests/GlobalUsings.cs
+++ b/PaperlessREST.Tests/GlobalUsings.cs
@@ -1,5 +1,6 @@
// System namespaces
+global using System.Diagnostics.CodeAnalysis;
global using System.IO.Abstractions;
global using System.Net;
global using System.Net.Http.Headers;
diff --git a/PaperlessREST.Tests/Integration/BatchOrchestratorIntegrationTests.cs b/PaperlessREST.Tests/Integration/BatchOrchestratorIntegrationTests.cs
index ab615ab..2f12731 100644
--- a/PaperlessREST.Tests/Integration/BatchOrchestratorIntegrationTests.cs
+++ b/PaperlessREST.Tests/Integration/BatchOrchestratorIntegrationTests.cs
@@ -31,7 +31,7 @@ await _fileSystem.File.WriteAllTextAsync(
TestContext.Current.CancellationToken);
// Act
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
// Assert
_fileSystem.Directory.GetFiles(_paths.Archive).Should().ContainSingle();
@@ -57,7 +57,7 @@ await _fileSystem.File.WriteAllTextAsync(
#region Fields
- private static readonly IJobCancellationToken TestToken = new TestJobCancellationToken();
+ private static readonly IJobCancellationToken s_testToken = new TestJobCancellationToken();
private readonly IDbContextFactory _dbFactory;
private readonly IFileSystem _fileSystem;
@@ -97,7 +97,7 @@ await _fileSystem.File.WriteAllTextAsync(
TestContext.Current.CancellationToken);
// Act
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
// Assert
_fileSystem.Directory.GetFiles(_paths.Archive)
@@ -111,7 +111,7 @@ public async Task ProcessAsync_OrphanClaimedFile_Processes()
{
// Arrange
Guid docId = await SeedDocumentAsync($"{TestFilePrefix}-orphan.pdf");
- string xmlContent = CreateXmlContent(DateOnly.FromDateTime(DateTime.UtcNow), (docId, 25));
+ string xmlContent = CreateXmlContent(DateOnly.FromDateTime(TimeProvider.System.GetUtcNow().UtcDateTime), (docId, 25));
string claimedPath = _fileSystem.Path.Combine(_paths.Input, "orphan.xml.processing");
await _fileSystem.File.WriteAllTextAsync(
@@ -120,7 +120,7 @@ await _fileSystem.File.WriteAllTextAsync(
TestContext.Current.CancellationToken);
// Act
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
// Assert
_fileSystem.Directory.GetFiles(_paths.Input).Should().BeEmpty("orphan should be processed");
@@ -140,7 +140,7 @@ await _fileSystem.File.WriteAllTextAsync(
TestContext.Current.CancellationToken);
// Act
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
// Assert
_fileSystem.Directory.GetFiles(_paths.Input)
@@ -157,7 +157,7 @@ public async Task ProcessAsync_InputDirectoryMissing_CreatesDirectoryAndContinue
_fileSystem.Directory.Exists(_paths.Input).Should().BeFalse("precondition: input dir should be deleted");
// Act
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
// Assert
_fileSystem.Directory.Exists(_paths.Input).Should().BeTrue("orchestrator should create input dir if missing");
@@ -171,7 +171,7 @@ public async Task ProcessAsync_ValidXmlFile_MovesToArchive()
await CreateXmlFileAsync("daily-report.xml", (docId, 15));
// Act
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
// Assert
_fileSystem.Directory.GetFiles(_paths.Input).Should().BeEmpty("file should be moved out of input");
@@ -194,7 +194,7 @@ public async Task ProcessAsync_MultipleDocumentsInOneFile_AggregatesAndUpserts()
await CreateXmlFileAsync("multi-doc.xml", (doc1, 10), (doc2, 20));
// Act
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
// Assert
_fileSystem.Directory.GetFiles(_paths.Archive).Should().ContainSingle();
@@ -210,7 +210,7 @@ public async Task ProcessAsync_DuplicateDocumentIdInSameFile_Aggregates()
await CreateXmlFileAsync("duplicates.xml", (docId, 5), (docId, 8));
// Act
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
// Assert
_fileSystem.Directory.GetFiles(_paths.Archive).Should().ContainSingle();
@@ -224,10 +224,10 @@ public async Task ProcessAsync_SameDocumentProcessedTwice_AccumulatesCounts()
Guid docId = await SeedDocumentAsync($"{TestFilePrefix}-monthly.pdf");
await CreateXmlFileAsync("morning.xml", (docId, 5));
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
await CreateXmlFileAsync("afternoon.xml", (docId, 8));
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
// Assert
await VerifyAccessRecordAsync(docId, 13, "should accumulate 5 + 8");
@@ -248,7 +248,7 @@ public async Task ProcessAsync_UnknownDocumentId_FiltersAndArchives()
await CreateXmlFileAsync("mixed.xml", (knownId, 10), (unknownId, 99));
// Act
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
// Assert
_fileSystem.Directory.GetFiles(_paths.Archive).Should().ContainSingle("file should still archive");
@@ -270,7 +270,7 @@ public async Task ProcessAsync_AllDocumentsUnknown_StillArchives()
await CreateXmlFileAsync("all-unknown.xml", (unknownId1, 10), (unknownId2, 20));
// Act
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
// Assert
_fileSystem.Directory.GetFiles(_paths.Archive).Should().ContainSingle("file should archive");
@@ -296,7 +296,7 @@ await _fileSystem.File.WriteAllTextAsync(
TestContext.Current.CancellationToken);
// Act
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
// Assert
_fileSystem.Directory.GetFiles(_paths.Error).Should().ContainSingle("malformed XML should go to error");
@@ -313,7 +313,7 @@ await _fileSystem.File.WriteAllTextAsync(
TestContext.Current.CancellationToken);
// Act
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
// Assert
_fileSystem.Directory.GetFiles(_paths.Error).Should().ContainSingle();
@@ -338,7 +338,7 @@ await _fileSystem.File.WriteAllTextAsync(
TestContext.Current.CancellationToken);
// Act
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
// Assert
_fileSystem.Directory.GetFiles(_paths.Error).Should().ContainSingle();
@@ -362,7 +362,7 @@ await _fileSystem.File.WriteAllTextAsync(
TestContext.Current.CancellationToken);
// Act
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
// Assert
_fileSystem.Directory.GetFiles(_paths.Error).Should().ContainSingle();
@@ -386,7 +386,7 @@ await _fileSystem.File.WriteAllTextAsync(
TestContext.Current.CancellationToken);
// Act
- await _orchestrator.ProcessAsync(TestToken);
+ await _orchestrator.ProcessAsync(s_testToken);
// Assert
_fileSystem.Directory.GetFiles(_paths.Error).Should().ContainSingle();
@@ -413,7 +413,7 @@ private async Task SeedDocumentAsync(string fileName)
private async Task CreateXmlFileAsync(string fileName, params (Guid id, int count)[] documents)
{
- DateOnly date = DateOnly.FromDateTime(DateTime.UtcNow);
+ DateOnly date = DateOnly.FromDateTime(TimeProvider.System.GetUtcNow().UtcDateTime);
string xmlContent = CreateXmlContent(date, documents);
await _fileSystem.File.WriteAllTextAsync(
_fileSystem.Path.Combine(_paths.Input, fileName),
@@ -438,7 +438,7 @@ private async Task VerifyAccessRecordAsync(Guid documentId, long expectedCount,
{
await using DocumentPersistence db = await _dbFactory.CreateDbContextAsync(
TestContext.Current.CancellationToken);
- DateOnly date = DateOnly.FromDateTime(DateTime.UtcNow);
+ DateOnly date = DateOnly.FromDateTime(TimeProvider.System.GetUtcNow().UtcDateTime);
DailyDocumentAccess? access = await db.DailyDocumentAccesses
.FirstOrDefaultAsync(
@@ -446,7 +446,7 @@ private async Task VerifyAccessRecordAsync(Guid documentId, long expectedCount,
TestContext.Current.CancellationToken);
access.Should().NotBeNull(because ?? "record should exist");
- access!.AccessCount.Should().Be(expectedCount, because ?? "access count should match");
+ access.AccessCount.Should().Be(expectedCount, because ?? "access count should match");
}
private void EnsureCleanDirectories()
diff --git a/PaperlessREST.Tests/Integration/DocumentAccessRepositoryIntegrationTests.cs b/PaperlessREST.Tests/Integration/DocumentAccessRepositoryIntegrationTests.cs
index 1902f65..c459f81 100644
--- a/PaperlessREST.Tests/Integration/DocumentAccessRepositoryIntegrationTests.cs
+++ b/PaperlessREST.Tests/Integration/DocumentAccessRepositoryIntegrationTests.cs
@@ -70,7 +70,8 @@ private async Task SeedDocumentAsync(string fileName)
private readonly DatabaseFixture _fixture;
private readonly List _createdDocIds = [];
private AsyncServiceScope _scope;
- private IDocumentAccessRepository _repository = null!;
+ private IDocumentAccessRepository? _repository;
+ private IDocumentAccessRepository Repository => _repository ?? throw new InvalidOperationException("Repository not initialized.");
#endregion
@@ -122,7 +123,7 @@ public async Task GetExistingDocumentIdsAsync_ReturnsOnlyExistingIds()
Guid nonExistingId = Guid.CreateVersion7();
// Act
- Guid[] result = await _repository.GetExistingDocumentIdsAsync(
+ Guid[] result = await Repository.GetExistingDocumentIdsAsync(
[existingId, nonExistingId],
TestContext.Current.CancellationToken);
@@ -137,7 +138,7 @@ public async Task GetExistingDocumentIdsAsync_EmptyInput_ReturnsEmptyArray()
Guid[] emptyIds = [];
// Act
- Guid[] result = await _repository.GetExistingDocumentIdsAsync(
+ Guid[] result = await Repository.GetExistingDocumentIdsAsync(
emptyIds,
TestContext.Current.CancellationToken);
@@ -154,7 +155,7 @@ public async Task GetExistingDocumentIdsAsync_AllIdsExist_ReturnsAll()
Guid id3 = await SeedDocumentAsync($"{TestFilePrefix}-all3-{Guid.NewGuid():N}.pdf");
// Act
- Guid[] result = await _repository.GetExistingDocumentIdsAsync(
+ Guid[] result = await Repository.GetExistingDocumentIdsAsync(
[id1, id2, id3],
TestContext.Current.CancellationToken);
@@ -173,7 +174,7 @@ public async Task GetExistingDocumentIdsAsync_NoIdsExist_ReturnsEmptyArray()
Guid nonExisting2 = Guid.CreateVersion7();
// Act
- Guid[] result = await _repository.GetExistingDocumentIdsAsync(
+ Guid[] result = await Repository.GetExistingDocumentIdsAsync(
[nonExisting1, nonExisting2],
TestContext.Current.CancellationToken);
@@ -189,11 +190,11 @@ public async Task GetExistingDocumentIdsAsync_NoIdsExist_ReturnsEmptyArray()
public async Task UpsertDailyAccessAsync_EmptyItems_DoesNothing()
{
// Arrange
- DateOnly date = DateOnly.FromDateTime(DateTime.UtcNow);
+ DateOnly date = DateOnly.FromDateTime(TimeProvider.System.GetUtcNow().UtcDateTime);
(Guid DocumentId, long AccessCount)[] emptyItems = [];
// Act - should return without error
- await _repository.UpsertDailyAccessAsync(date, emptyItems, TestContext.Current.CancellationToken);
+ await Repository.UpsertDailyAccessAsync(date, emptyItems, TestContext.Current.CancellationToken);
// Assert - no exception thrown, method returns early
}
@@ -203,21 +204,21 @@ public async Task UpsertDailyAccessAsync_InsertsNewRecord()
{
// Arrange
Guid docId = await SeedDocumentAsync($"{TestFilePrefix}-upsert-insert-{Guid.NewGuid():N}.pdf");
- DateOnly date = DateOnly.FromDateTime(DateTime.UtcNow);
- const long accessCount = 42;
+ DateOnly date = DateOnly.FromDateTime(TimeProvider.System.GetUtcNow().UtcDateTime);
+ const long AccessCount = 42;
try
{
// Act
- await _repository.UpsertDailyAccessAsync(
+ await Repository.UpsertDailyAccessAsync(
date,
- [(docId, accessCount)],
+ [(docId, AccessCount)],
TestContext.Current.CancellationToken);
// Assert
DailyDocumentAccess? record = await GetDailyAccessAsync(docId, date);
record.Should().NotBeNull();
- record!.AccessCount.Should().Be(accessCount);
+ record!.AccessCount.Should().Be(AccessCount);
record.DocumentId.Should().Be(docId);
record.LogDate.Should().Be(date);
}
@@ -232,29 +233,29 @@ public async Task UpsertDailyAccessAsync_UpdatesExistingRecord_IncrementsAccessC
{
// Arrange
Guid docId = await SeedDocumentAsync($"{TestFilePrefix}-upsert-update-{Guid.NewGuid():N}.pdf");
- DateOnly date = DateOnly.FromDateTime(DateTime.UtcNow);
- const long initialCount = 10;
- const long additionalCount = 25;
- const long expectedTotal = initialCount + additionalCount;
+ DateOnly date = DateOnly.FromDateTime(TimeProvider.System.GetUtcNow().UtcDateTime);
+ const long InitialCount = 10;
+ const long AdditionalCount = 25;
+ const long ExpectedTotal = InitialCount + AdditionalCount;
try
{
// Insert initial record
- await _repository.UpsertDailyAccessAsync(
+ await Repository.UpsertDailyAccessAsync(
date,
- [(docId, initialCount)],
+ [(docId, InitialCount)],
TestContext.Current.CancellationToken);
// Act - upsert again with additional count
- await _repository.UpsertDailyAccessAsync(
+ await Repository.UpsertDailyAccessAsync(
date,
- [(docId, additionalCount)],
+ [(docId, AdditionalCount)],
TestContext.Current.CancellationToken);
// Assert - count should be incremented
DailyDocumentAccess? record = await GetDailyAccessAsync(docId, date);
record.Should().NotBeNull();
- record!.AccessCount.Should().Be(expectedTotal);
+ record!.AccessCount.Should().Be(ExpectedTotal);
}
finally
{
@@ -268,16 +269,16 @@ public async Task UpsertDailyAccessAsync_MultipleItems_InsertsAll()
// Arrange
Guid docId1 = await SeedDocumentAsync($"{TestFilePrefix}-upsert-multi1-{Guid.NewGuid():N}.pdf");
Guid docId2 = await SeedDocumentAsync($"{TestFilePrefix}-upsert-multi2-{Guid.NewGuid():N}.pdf");
- DateOnly date = DateOnly.FromDateTime(DateTime.UtcNow);
- const long count1 = 100;
- const long count2 = 200;
+ DateOnly date = DateOnly.FromDateTime(TimeProvider.System.GetUtcNow().UtcDateTime);
+ const long Count1 = 100;
+ const long Count2 = 200;
try
{
// Act
- await _repository.UpsertDailyAccessAsync(
+ await Repository.UpsertDailyAccessAsync(
date,
- [(docId1, count1), (docId2, count2)],
+ [(docId1, Count1), (docId2, Count2)],
TestContext.Current.CancellationToken);
// Assert
@@ -285,10 +286,10 @@ await _repository.UpsertDailyAccessAsync(
DailyDocumentAccess? record2 = await GetDailyAccessAsync(docId2, date);
record1.Should().NotBeNull();
- record1!.AccessCount.Should().Be(count1);
+ record1!.AccessCount.Should().Be(Count1);
record2.Should().NotBeNull();
- record2!.AccessCount.Should().Be(count2);
+ record2!.AccessCount.Should().Be(Count2);
}
finally
{
@@ -302,22 +303,22 @@ public async Task UpsertDailyAccessAsync_SameDocumentDifferentDates_CreatesSepar
{
// Arrange
Guid docId = await SeedDocumentAsync($"{TestFilePrefix}-upsert-dates-{Guid.NewGuid():N}.pdf");
- DateOnly today = DateOnly.FromDateTime(DateTime.UtcNow);
+ DateOnly today = DateOnly.FromDateTime(TimeProvider.System.GetUtcNow().UtcDateTime);
DateOnly yesterday = today.AddDays(-1);
- const long todayCount = 50;
- const long yesterdayCount = 30;
+ const long TodayCount = 50;
+ const long YesterdayCount = 30;
try
{
// Act
- await _repository.UpsertDailyAccessAsync(
+ await Repository.UpsertDailyAccessAsync(
today,
- [(docId, todayCount)],
+ [(docId, TodayCount)],
TestContext.Current.CancellationToken);
- await _repository.UpsertDailyAccessAsync(
+ await Repository.UpsertDailyAccessAsync(
yesterday,
- [(docId, yesterdayCount)],
+ [(docId, YesterdayCount)],
TestContext.Current.CancellationToken);
// Assert - should have two separate records
@@ -325,10 +326,10 @@ await _repository.UpsertDailyAccessAsync(
DailyDocumentAccess? yesterdayRecord = await GetDailyAccessAsync(docId, yesterday);
todayRecord.Should().NotBeNull();
- todayRecord!.AccessCount.Should().Be(todayCount);
+ todayRecord!.AccessCount.Should().Be(TodayCount);
yesterdayRecord.Should().NotBeNull();
- yesterdayRecord!.AccessCount.Should().Be(yesterdayCount);
+ yesterdayRecord!.AccessCount.Should().Be(YesterdayCount);
}
finally
{
diff --git a/PaperlessREST.Tests/Integration/DocumentEndpointTests.cs b/PaperlessREST.Tests/Integration/DocumentEndpointTests.cs
index 5f62d71..ab29129 100644
--- a/PaperlessREST.Tests/Integration/DocumentEndpointTests.cs
+++ b/PaperlessREST.Tests/Integration/DocumentEndpointTests.cs
@@ -47,7 +47,7 @@ public async Task Upload_ValidPdf_Returns202WithLocation()
{
// Arrange
string uniqueFileName = $"{TestFilePrefix}-upload-{Guid.NewGuid():N}.pdf";
- MultipartFormDataContent content = await CreatePdfUploadAsync(uniqueFileName);
+ using MultipartFormDataContent content = await CreatePdfUploadAsync(uniqueFileName);
// Act
HttpResponseMessage response = await _fixture.Client.PostAsync(
@@ -165,6 +165,9 @@ private async Task SeedDocumentAsync(string fileName)
return entity.Id;
}
+ [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope",
+ Justification = "ByteArrayContent ownership transfers to MultipartFormDataContent on Add(); "
+ + "the inner try/catch covers the pre-transfer window. Standard .NET HttpClient pattern.")]
private async Task CreatePdfUploadAsync(string fileName)
{
string tempPath = Path.Combine(Path.GetTempPath(), $"test-{Guid.NewGuid():N}.pdf");
@@ -174,11 +177,28 @@ private async Task CreatePdfUploadAsync(string fileNam
File.Delete(pdfPath);
- MultipartFormDataContent content = new();
- ByteArrayContent byteContent = new(pdfBytes);
- byteContent.Headers.ContentType = MediaTypeHeaderValue.Parse(ContentTypePdf);
- content.Add(byteContent, "file", fileName);
- return content;
+ var content = new MultipartFormDataContent();
+ try
+ {
+ var fileContent = new ByteArrayContent(pdfBytes);
+ try
+ {
+ fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(ContentTypePdf);
+ content.Add(fileContent, "file", fileName);
+ }
+ catch
+ {
+ fileContent.Dispose();
+ throw;
+ }
+
+ return content;
+ }
+ catch
+ {
+ content.Dispose();
+ throw;
+ }
}
#endregion
diff --git a/PaperlessREST.Tests/Integration/DocumentRepositoryIntegrationTests.cs b/PaperlessREST.Tests/Integration/DocumentRepositoryIntegrationTests.cs
index 7698d0a..7427d82 100644
--- a/PaperlessREST.Tests/Integration/DocumentRepositoryIntegrationTests.cs
+++ b/PaperlessREST.Tests/Integration/DocumentRepositoryIntegrationTests.cs
@@ -28,7 +28,8 @@ public DocumentRepositoryIntegrationTests(DatabaseFixture fixture)
private readonly DatabaseFixture _fixture;
private AsyncServiceScope _scope;
- private IDocumentRepository _repository = null!;
+ private IDocumentRepository? _repository;
+ private IDocumentRepository Repository => _repository ?? throw new InvalidOperationException("Repository not initialized.");
#endregion
@@ -56,7 +57,7 @@ public async Task AddAsync_ValidDocument_PersistsToDatabase()
.Build();
// Act
- Document saved = await _repository.AddAsync(document, TestContext.Current.CancellationToken);
+ Document saved = await Repository.AddAsync(document, TestContext.Current.CancellationToken);
// Assert
saved.Id.Should().Be(document.Id);
@@ -84,7 +85,7 @@ public async Task AddAsync_SetsStoragePath()
.Build();
// Act
- Document saved = await _repository.AddAsync(document, TestContext.Current.CancellationToken);
+ Document saved = await Repository.AddAsync(document, TestContext.Current.CancellationToken);
// Assert
saved.StoragePath.Should().Be(StoragePath);
@@ -111,10 +112,10 @@ public async Task GetByIdAsync_ExistingDocument_ReturnsWithAllProperties()
.WithSummary(AiSummary)
.AsCompleted(OcrContent)
.Build();
- await _repository.AddAsync(original, TestContext.Current.CancellationToken);
+ await Repository.AddAsync(original, TestContext.Current.CancellationToken);
// Act
- Document? found = await _repository.GetByIdAsync(original.Id, TestContext.Current.CancellationToken);
+ Document? found = await Repository.GetByIdAsync(original.Id, TestContext.Current.CancellationToken);
// Assert
found.Should().NotBeNull();
@@ -130,7 +131,7 @@ public async Task GetByIdAsync_NonExistent_ReturnsNull()
Guid nonExistentId = Guid.CreateVersion7();
// Act
- Document? result = await _repository.GetByIdAsync(nonExistentId, TestContext.Current.CancellationToken);
+ Document? result = await Repository.GetByIdAsync(nonExistentId, TestContext.Current.CancellationToken);
// Assert
result.Should().BeNull();
@@ -147,12 +148,12 @@ public async Task UpdateAsync_MarkAsCompleted_UpdatesStatusAndContent()
Document original = new DocumentBuilder()
.WithFileName($"{TestFilePrefix}-update-{Guid.NewGuid():N}.pdf")
.Build();
- Document saved = await _repository.AddAsync(original, TestContext.Current.CancellationToken);
+ Document saved = await Repository.AddAsync(original, TestContext.Current.CancellationToken);
saved.MarkAsCompleted(OcrContent, TimeProvider.System);
// Act
- bool updated = await _repository.UpdateAsync(saved, TestContext.Current.CancellationToken);
+ bool updated = await Repository.UpdateAsync(saved, TestContext.Current.CancellationToken);
// Assert
updated.Should().BeTrue();
@@ -175,12 +176,12 @@ public async Task UpdateAsync_MarkAsFailed_UpdatesStatus()
Document original = new DocumentBuilder()
.WithFileName($"{TestFilePrefix}-fail-{Guid.NewGuid():N}.pdf")
.Build();
- Document saved = await _repository.AddAsync(original, TestContext.Current.CancellationToken);
+ Document saved = await Repository.AddAsync(original, TestContext.Current.CancellationToken);
saved.MarkAsFailed(TimeProvider.System);
// Act
- bool updated = await _repository.UpdateAsync(saved, TestContext.Current.CancellationToken);
+ bool updated = await Repository.UpdateAsync(saved, TestContext.Current.CancellationToken);
// Assert
updated.Should().BeTrue();
@@ -205,7 +206,7 @@ public async Task UpdateAsync_NonExistent_ReturnsFalse()
.Build();
// Act
- bool result = await _repository.UpdateAsync(document, TestContext.Current.CancellationToken);
+ bool result = await Repository.UpdateAsync(document, TestContext.Current.CancellationToken);
// Assert
result.Should().BeFalse();
@@ -222,15 +223,15 @@ public async Task DeleteAsync_ExistingDocument_RemovesFromDatabase()
Document document = new DocumentBuilder()
.WithFileName($"{TestFilePrefix}-delete-{Guid.NewGuid():N}.pdf")
.Build();
- await _repository.AddAsync(document, TestContext.Current.CancellationToken);
+ await Repository.AddAsync(document, TestContext.Current.CancellationToken);
// Act
- bool deleted = await _repository.DeleteAsync(document.Id, TestContext.Current.CancellationToken);
+ bool deleted = await Repository.DeleteAsync(document.Id, TestContext.Current.CancellationToken);
// Assert
deleted.Should().BeTrue();
- Document? found = await _repository.GetByIdAsync(document.Id, TestContext.Current.CancellationToken);
+ Document? found = await Repository.GetByIdAsync(document.Id, TestContext.Current.CancellationToken);
found.Should().BeNull();
}
@@ -241,7 +242,7 @@ public async Task DeleteAsync_NonExistent_ReturnsFalse()
Guid nonExistentId = Guid.CreateVersion7();
// Act
- bool deleted = await _repository.DeleteAsync(nonExistentId, TestContext.Current.CancellationToken);
+ bool deleted = await Repository.DeleteAsync(nonExistentId, TestContext.Current.CancellationToken);
// Assert
deleted.Should().BeFalse();
@@ -259,12 +260,12 @@ public async Task UpdateSummaryAsync_ExistingDocument_UpdatesOnlySummaryFields()
.WithFileName($"{TestFilePrefix}-summary-{Guid.NewGuid():N}.pdf")
.AsCompleted(OcrContent)
.Build();
- await _repository.AddAsync(document, TestContext.Current.CancellationToken);
+ await Repository.AddAsync(document, TestContext.Current.CancellationToken);
- DateTimeOffset generatedAt = DateTimeOffset.UtcNow;
+ DateTimeOffset generatedAt = TimeProvider.System.GetUtcNow();
// Act
- bool updated = await _repository.UpdateSummaryAsync(
+ bool updated = await Repository.UpdateSummaryAsync(
document.Id,
AiSummary,
generatedAt,
@@ -273,7 +274,7 @@ public async Task UpdateSummaryAsync_ExistingDocument_UpdatesOnlySummaryFields()
// Assert
updated.Should().BeTrue();
- Document? found = await _repository.GetByIdAsync(document.Id, TestContext.Current.CancellationToken);
+ Document? found = await Repository.GetByIdAsync(document.Id, TestContext.Current.CancellationToken);
found.Should().NotBeNull();
found!.Summary.Should().Be(AiSummary);
found.SummaryGeneratedAt.Should().BeCloseTo(generatedAt, TimeSpan.FromSeconds(1));
@@ -289,10 +290,10 @@ public async Task UpdateSummaryAsync_NonExistent_ReturnsFalse()
Guid nonExistentId = Guid.CreateVersion7();
// Act
- bool updated = await _repository.UpdateSummaryAsync(
+ bool updated = await Repository.UpdateSummaryAsync(
nonExistentId,
AiSummary,
- DateTimeOffset.UtcNow,
+ TimeProvider.System.GetUtcNow(),
TestContext.Current.CancellationToken);
// Assert
@@ -311,18 +312,18 @@ public async Task GetDocumentsPagedAsync_ReturnsNewestFirst()
// Add documents with slight delays to ensure distinct GUIDv7s
Document oldest = new DocumentBuilder().WithFileName($"{testPrefix}-old.pdf").Build();
- await _repository.AddAsync(oldest, TestContext.Current.CancellationToken);
+ await Repository.AddAsync(oldest, TestContext.Current.CancellationToken);
await Task.Delay(10, TestContext.Current.CancellationToken);
Document middle = new DocumentBuilder().WithFileName($"{testPrefix}-mid.pdf").Build();
- await _repository.AddAsync(middle, TestContext.Current.CancellationToken);
+ await Repository.AddAsync(middle, TestContext.Current.CancellationToken);
await Task.Delay(10, TestContext.Current.CancellationToken);
Document newest = new DocumentBuilder().WithFileName($"{testPrefix}-new.pdf").Build();
- await _repository.AddAsync(newest, TestContext.Current.CancellationToken);
+ await Repository.AddAsync(newest, TestContext.Current.CancellationToken);
// Act
- (List results, bool hasMore) = await _repository
+ (List results, bool hasMore) = await Repository
.GetDocumentsPagedAsync(50, null, TestContext.Current.CancellationToken);
List filtered = results.Where(d => d.FileName.StartsWith(testPrefix, StringComparison.Ordinal)).ToList();
@@ -342,14 +343,14 @@ public async Task GetDocumentsPagedAsync_RespectsPageSize()
for (int i = 0; i < 5; i++)
{
- await _repository.AddAsync(
+ await Repository.AddAsync(
new DocumentBuilder().WithFileName($"{testPrefix}-{i}.pdf").Build(),
TestContext.Current.CancellationToken);
await Task.Delay(5, TestContext.Current.CancellationToken); // Ensure distinct GUIDv7s
}
// Act
- (List results, bool hasMore) = await _repository
+ (List results, bool hasMore) = await Repository
.GetDocumentsPagedAsync(3, null, TestContext.Current.CancellationToken);
// Assert
@@ -367,18 +368,18 @@ public async Task GetDocumentsPagedAsync_WithCursor_ReturnsNextPage()
for (int i = 0; i < 5; i++)
{
Document doc = new DocumentBuilder().WithFileName($"{testPrefix}-{i}.pdf").Build();
- Document added = await _repository.AddAsync(doc, TestContext.Current.CancellationToken);
+ Document added = await Repository.AddAsync(doc, TestContext.Current.CancellationToken);
addedDocs.Add(added);
await Task.Delay(5, TestContext.Current.CancellationToken);
}
// Act - Get first page
- (List firstPage, bool hasMoreFirst) = await _repository
+ (List firstPage, bool hasMoreFirst) = await Repository
.GetDocumentsPagedAsync(2, null, TestContext.Current.CancellationToken);
// Get second page using cursor from first page
Guid cursor = firstPage[^1].Id;
- (List secondPage, bool hasMoreSecond) = await _repository
+ (List secondPage, bool hasMoreSecond) = await Repository
.GetDocumentsPagedAsync(2, cursor, TestContext.Current.CancellationToken);
// Assert
@@ -397,14 +398,14 @@ public async Task GetDocumentsPagedAsync_LastPage_HasMoreIsFalse()
for (int i = 0; i < 3; i++)
{
- await _repository.AddAsync(
+ await Repository.AddAsync(
new DocumentBuilder().WithFileName($"{testPrefix}-{i}.pdf").Build(),
TestContext.Current.CancellationToken);
await Task.Delay(5, TestContext.Current.CancellationToken);
}
// Act - Request more than available
- (List results, bool hasMore) = await _repository
+ (List results, bool hasMore) = await Repository
.GetDocumentsPagedAsync(100, null, TestContext.Current.CancellationToken);
// Assert
diff --git a/PaperlessREST.Tests/Integration/SharedRestContainerFixture.cs b/PaperlessREST.Tests/Integration/SharedRestContainerFixture.cs
index 4523e6e..4748265 100644
--- a/PaperlessREST.Tests/Integration/SharedRestContainerFixture.cs
+++ b/PaperlessREST.Tests/Integration/SharedRestContainerFixture.cs
@@ -68,7 +68,8 @@ public SharedRestContainerFixture()
private readonly PostgreSqlContainer _postgres;
private readonly RabbitMqContainer _rabbit;
- private WebApplicationFactory _factory = null!;
+ private WebApplicationFactory? _factory;
+ private WebApplicationFactory Factory => _factory ?? throw new InvalidOperationException("Factory not initialized.");
#endregion
@@ -103,37 +104,17 @@ await Task.WhenAll(
Environment.SetEnvironmentVariable("ELASTICSEARCH__URI",
$"http://{_elastic.Hostname}:{_elastic.GetMappedPublicPort(9200)}");
- IMinioClient? minioClient = new MinioClient()
+ using MinioClient minioClient = new();
+ minioClient
.WithEndpoint(minioEndpoint)
.WithCredentials(_minio.GetAccessKey(), _minio.GetSecretKey())
.Build();
await minioClient.MakeBucketAsync(new MakeBucketArgs().WithBucket(_bucketName));
- _factory = new WebApplicationFactory()
- .WithWebHostBuilder(builder =>
- {
- builder.ConfigureTestServices(services =>
- {
- services.RemoveAll();
-
- services.RemoveAll>();
-
- NpgsqlDataSource dataSource = new NpgsqlDataSourceBuilder(_postgres.GetConnectionString())
- .MapEnum("document_status")
- .Build();
-
- services.AddPooledDbContextFactory(opts =>
- opts.UseNpgsql(dataSource));
-
- services.RemoveAll();
- services.AddSingleton(new MemoryStorage());
+ _factory = new ConfiguredWebApplicationFactory(_postgres.GetConnectionString());
- services.AddFakeLogging();
- });
- });
-
- Client = _factory.CreateClient();
- Services = _factory.Services;
+ Client = Factory.CreateClient();
+ Services = Factory.Services;
DbFactory = Services.GetRequiredService>();
await using DocumentPersistence db = await DbFactory.CreateDbContextAsync();
@@ -142,7 +123,8 @@ await Task.WhenAll(
public async ValueTask DisposeAsync()
{
- await _factory.DisposeAsync();
+ if (_factory is not null)
+ await _factory.DisposeAsync();
await Task.WhenAll(
_postgres.DisposeAsync().AsTask(),
_rabbit.DisposeAsync().AsTask(),
@@ -152,4 +134,30 @@ await Task.WhenAll(
}
#endregion
+
+ private sealed class ConfiguredWebApplicationFactory(string postgresConnectionString)
+ : WebApplicationFactory
+ {
+ protected override void ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBuilder builder)
+ {
+ builder.ConfigureTestServices(services =>
+ {
+ services.RemoveAll();
+
+ services.RemoveAll>();
+
+ NpgsqlDataSource dataSource = new NpgsqlDataSourceBuilder(postgresConnectionString)
+ .MapEnum("document_status")
+ .Build();
+
+ services.AddPooledDbContextFactory(opts =>
+ opts.UseNpgsql(dataSource));
+
+ services.RemoveAll();
+ services.AddSingleton(new MemoryStorage());
+
+ services.AddFakeLogging();
+ });
+ }
+ }
}
diff --git a/PaperlessREST.Tests/Unit/BatchOrchestratorTests.cs b/PaperlessREST.Tests/Unit/BatchOrchestratorTests.cs
index e873138..f0d1576 100644
--- a/PaperlessREST.Tests/Unit/BatchOrchestratorTests.cs
+++ b/PaperlessREST.Tests/Unit/BatchOrchestratorTests.cs
@@ -40,7 +40,7 @@ public ProcessAsync()
_logger = new FakeLogger(_logCollector);
// Setup default time provider behavior - loose mock, not verified
- _timeProvider.Setup(t => t.GetUtcNow()).Returns(DateTimeOffset.UtcNow);
+ _timeProvider.Setup(t => t.GetUtcNow()).Returns(TimeProvider.System.GetUtcNow());
// Initialize directory structure
_fileSystem.Directory.CreateDirectory(InputPath);
@@ -653,7 +653,7 @@ await sut.ProcessFileAsync(
private BatchOrchestrator CreateSut()
{
- _time.Setup(t => t.GetUtcNow()).Returns(DateTimeOffset.UtcNow);
+ _time.Setup(t => t.GetUtcNow()).Returns(TimeProvider.System.GetUtcNow());
return new BatchOrchestrator(
Options.Create(CreateOptions()),
_fs.Object,
diff --git a/PaperlessREST.Tests/Unit/DocumentServiceTests.cs b/PaperlessREST.Tests/Unit/DocumentServiceTests.cs
index 30f4d48..6f5dcdc 100644
--- a/PaperlessREST.Tests/Unit/DocumentServiceTests.cs
+++ b/PaperlessREST.Tests/Unit/DocumentServiceTests.cs
@@ -240,7 +240,7 @@ public async Task UpdateDocumentSummaryAsync_DocumentFound_UpdatesAndReturnsTrue
{
// Arrange
Document doc = new DocumentBuilder().AsCompleted().Build();
- DateTimeOffset generatedAt = DateTimeOffset.UtcNow;
+ DateTimeOffset generatedAt = TimeProvider.System.GetUtcNow();
_repository.Setup(r => r.UpdateSummaryAsync(doc.Id, GenAiSummary, generatedAt,
It.IsAny()))
@@ -262,7 +262,7 @@ public async Task UpdateDocumentSummaryAsync_DocumentFound_LogsSummaryLength()
{
// Arrange
Document doc = new DocumentBuilder().AsCompleted().Build();
- DateTimeOffset generatedAt = DateTimeOffset.UtcNow;
+ DateTimeOffset generatedAt = TimeProvider.System.GetUtcNow();
_repository.Setup(r => r.UpdateSummaryAsync(doc.Id, GenAiSummary, generatedAt,
It.IsAny()))
@@ -290,7 +290,7 @@ public async Task UpdateDocumentSummaryAsync_DocumentNotFound_ReturnsFalse()
{
// Arrange
Guid missingId = Guid.CreateVersion7();
- DateTimeOffset generatedAt = DateTimeOffset.UtcNow;
+ DateTimeOffset generatedAt = TimeProvider.System.GetUtcNow();
_repository.Setup(r => r.UpdateSummaryAsync(missingId, GenAiSummary, generatedAt,
It.IsAny()))
@@ -312,7 +312,7 @@ public async Task UpdateDocumentSummaryAsync_DocumentNotFound_LogsWarning()
{
// Arrange
Guid missingId = Guid.CreateVersion7();
- DateTimeOffset generatedAt = DateTimeOffset.UtcNow;
+ DateTimeOffset generatedAt = TimeProvider.System.GetUtcNow();
_repository.Setup(r => r.UpdateSummaryAsync(missingId, GenAiSummary, generatedAt,
It.IsAny()))
@@ -394,7 +394,7 @@ public async Task SearchDocumentsAsync_DelegatesToSearchService()
Id = Guid.CreateVersion7(),
FileName = "result1.pdf",
Status = "Completed",
- CreatedAt = DateTimeOffset.UtcNow,
+ CreatedAt = TimeProvider.System.GetUtcNow(),
Content = "Content 1"
},
new DocumentSearchResult
@@ -402,7 +402,7 @@ public async Task SearchDocumentsAsync_DelegatesToSearchService()
Id = Guid.CreateVersion7(),
FileName = "result2.pdf",
Status = "Completed",
- CreatedAt = DateTimeOffset.UtcNow,
+ CreatedAt = TimeProvider.System.GetUtcNow(),
Content = "Content 2"
}
];
diff --git a/PaperlessREST.Tests/Unit/DocumentTests.cs b/PaperlessREST.Tests/Unit/DocumentTests.cs
index 2e0b5ab..a63abf6 100644
--- a/PaperlessREST.Tests/Unit/DocumentTests.cs
+++ b/PaperlessREST.Tests/Unit/DocumentTests.cs
@@ -10,8 +10,8 @@ public sealed class DocumentTests
private const string TestContent = "content";
private const string TestSummary = "S";
- private static readonly DateTimeOffset FixedTime = new(2025, 6, 15, 10, 30, 0, TimeSpan.Zero);
- private readonly FakeTimeProvider _timeProvider = new(FixedTime);
+ private static readonly DateTimeOffset s_fixedTime = new(2025, 6, 15, 10, 30, 0, TimeSpan.Zero);
+ private readonly FakeTimeProvider _timeProvider = new(s_fixedTime);
public static IEnumerable> CompleteNotPending()
{
@@ -39,8 +39,8 @@ public void CreateFromUpload_SetsDefaults()
document.Id.Should().NotBeEmpty();
document.FileName.Should().Be(TestFileName);
document.Status.Should().Be(DocumentStatus.Pending);
- document.CreatedAt.Should().Be(FixedTime);
- document.StoragePath.Should().Match($"documents/{FixedTime.UtcDateTime:yyyy-MM}/{document.Id}.pdf");
+ document.CreatedAt.Should().Be(s_fixedTime);
+ document.StoragePath.Should().Match($"documents/{s_fixedTime.UtcDateTime:yyyy-MM}/{document.Id}.pdf");
document.Content.Should().BeNull();
document.ProcessedAt.Should().BeNull();
}
@@ -73,7 +73,7 @@ public void MarkAsCompleted_WhenPending_Transitions()
result.IsError.Should().BeFalse();
document.Status.Should().Be(DocumentStatus.Completed);
document.Content.Should().Be(TestContent);
- document.ProcessedAt.Should().Be(FixedTime);
+ document.ProcessedAt.Should().Be(s_fixedTime);
}
[Theory]
@@ -104,7 +104,7 @@ public void MarkAsFailed_WhenPending_Transitions()
result.IsError.Should().BeFalse();
document.Status.Should().Be(DocumentStatus.Failed);
document.Content.Should().BeNull();
- document.ProcessedAt.Should().Be(FixedTime);
+ document.ProcessedAt.Should().Be(s_fixedTime);
}
[Theory]
@@ -115,7 +115,7 @@ public void UpdateSummary_Works(DocumentStatus status)
{
// Arrange
Document document = new DocumentBuilder().WithStatus(status).Build();
- DateTimeOffset at = DateTimeOffset.UtcNow;
+ DateTimeOffset at = TimeProvider.System.GetUtcNow();
// Act
document.UpdateSummary(TestSummary, at);
diff --git a/PaperlessREST.Tests/Unit/EndpointsTests.cs b/PaperlessREST.Tests/Unit/EndpointsTests.cs
index a4441c0..6d26a80 100644
--- a/PaperlessREST.Tests/Unit/EndpointsTests.cs
+++ b/PaperlessREST.Tests/Unit/EndpointsTests.cs
@@ -150,7 +150,7 @@ public async Task SearchDocuments_WhenMatchesFound_ReturnsOkWithResults()
Id = PairA.Doc.Id,
FileName = PairA.Doc.FileName,
Status = StatusCompleted,
- CreatedAt = DateTimeOffset.UtcNow,
+ CreatedAt = TimeProvider.System.GetUtcNow(),
Content = ContentA
},
new()
@@ -158,7 +158,7 @@ public async Task SearchDocuments_WhenMatchesFound_ReturnsOkWithResults()
Id = PairB.Doc.Id,
FileName = PairB.Doc.FileName,
Status = StatusPending,
- CreatedAt = DateTimeOffset.UtcNow
+ CreatedAt = TimeProvider.System.GetUtcNow()
}
];
_service.Setup(s => s.SearchDocumentsAsync(query.Query, query.Limit, TestContext.Current.CancellationToken))
@@ -299,22 +299,22 @@ private static async IAsyncEnumerable ToAsyncEnumerable(IEnumerable sou
private static class PairA
{
- private static readonly DocumentBuilder Builder = new DocumentBuilder()
+ private static readonly DocumentBuilder s_builder = new DocumentBuilder()
.WithFileName(FileA)
.WithStatus(DocumentStatus.Completed)
.WithContent(ContentA);
- public static readonly Document Doc = Builder.Build();
- public static readonly DocumentDto Dto = Builder.BuildDto();
+ public static readonly Document Doc = s_builder.Build();
+ public static readonly DocumentDto Dto = s_builder.BuildDto();
}
private static class PairB
{
- private static readonly DocumentBuilder Builder = new DocumentBuilder()
+ private static readonly DocumentBuilder s_builder = new DocumentBuilder()
.WithFileName(FileB)
.WithStatus(DocumentStatus.Pending);
- public static readonly Document Doc = Builder.Build();
- public static readonly DocumentDto Dto = Builder.BuildDto();
+ public static readonly Document Doc = s_builder.Build();
+ public static readonly DocumentDto Dto = s_builder.BuildDto();
}
}
diff --git a/PaperlessREST.Tests/Unit/ExceptionHandlerTests.cs b/PaperlessREST.Tests/Unit/ExceptionHandlerTests.cs
index 16c07dd..02661cc 100644
--- a/PaperlessREST.Tests/Unit/ExceptionHandlerTests.cs
+++ b/PaperlessREST.Tests/Unit/ExceptionHandlerTests.cs
@@ -231,7 +231,7 @@ public async Task TryHandleAsync_PassesHttpContextToWriter()
{
// Arrange
HttpContext context = _setup.CreateHttpContext();
- Exception exception = new(ExceptionHandlerConstants.TestExceptionMessage);
+ InvalidOperationException exception = new(ExceptionHandlerConstants.TestExceptionMessage);
ProblemDetailsContext? capturedContext = null;
_setup.ProblemDetails.Setup(p => p.TryWriteAsync(It.IsAny()))
diff --git a/PaperlessREST.Tests/Unit/GenAiResultListenerTests.cs b/PaperlessREST.Tests/Unit/GenAiResultListenerTests.cs
index d5d12c9..2c25433 100644
--- a/PaperlessREST.Tests/Unit/GenAiResultListenerTests.cs
+++ b/PaperlessREST.Tests/Unit/GenAiResultListenerTests.cs
@@ -69,7 +69,7 @@ private static GenAIEvent CreateGenAiEvent(
string? summary = ValidSummary,
DateTimeOffset? generatedAt = null,
string? errorMessage = null) =>
- new(documentId ?? Guid.CreateVersion7(), summary, generatedAt ?? DateTimeOffset.UtcNow, errorMessage);
+ new(documentId ?? Guid.CreateVersion7(), summary, generatedAt ?? TimeProvider.System.GetUtcNow(), errorMessage);
// ═══════════════════════════════════════════════════════════════
// TESTS: ProcessGenAiEventAsync - Success Path (With Summary)
@@ -91,7 +91,7 @@ public async Task ProcessGenAiEventAsync_WithSummary_UpdatesDocumentAndPublishes
_sseStream.Setup(s => s.Publish(genAiEvent));
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- GenAiResultListener sut = CreateSut();
+ using GenAiResultListener sut = CreateSut();
// Act
await sut.ProcessGenAiEventAsync(genAiEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -116,7 +116,7 @@ public async Task ProcessGenAiEventAsync_WithSummary_LogsSuccessfulUpdate()
_sseStream.Setup(s => s.Publish(genAiEvent));
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- GenAiResultListener sut = CreateSut();
+ using GenAiResultListener sut = CreateSut();
// Act
await sut.ProcessGenAiEventAsync(genAiEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -147,7 +147,7 @@ public async Task ProcessGenAiEventAsync_WithSummary_DocumentNotFound_LogsWarnin
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- GenAiResultListener sut = CreateSut();
+ using GenAiResultListener sut = CreateSut();
// Act
await sut.ProcessGenAiEventAsync(genAiEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -174,7 +174,7 @@ public async Task ProcessGenAiEventAsync_DocumentNotFound_DoesNotPublishToSse()
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- GenAiResultListener sut = CreateSut();
+ using GenAiResultListener sut = CreateSut();
// Act
await sut.ProcessGenAiEventAsync(genAiEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -196,7 +196,7 @@ public async Task ProcessGenAiEventAsync_EmptySummary_LogsWarningPublishesAndAck
_sseStream.Setup(s => s.Publish(genAiEvent));
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- GenAiResultListener sut = CreateSut();
+ using GenAiResultListener sut = CreateSut();
// Act
await sut.ProcessGenAiEventAsync(genAiEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -218,7 +218,7 @@ public async Task ProcessGenAiEventAsync_NullSummary_LogsWarningPublishesAndAcks
_sseStream.Setup(s => s.Publish(genAiEvent));
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- GenAiResultListener sut = CreateSut();
+ using GenAiResultListener sut = CreateSut();
// Act
await sut.ProcessGenAiEventAsync(genAiEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -240,7 +240,7 @@ public async Task ProcessGenAiEventAsync_WhitespaceSummary_TreatedAsEmpty()
_sseStream.Setup(s => s.Publish(genAiEvent));
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- GenAiResultListener sut = CreateSut();
+ using GenAiResultListener sut = CreateSut();
// Act
await sut.ProcessGenAiEventAsync(genAiEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -262,7 +262,7 @@ public async Task ProcessGenAiEventAsync_EmptySummary_LogsWarningWithErrorMessag
_sseStream.Setup(s => s.Publish(genAiEvent));
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- GenAiResultListener sut = CreateSut();
+ using GenAiResultListener sut = CreateSut();
// Act
await sut.ProcessGenAiEventAsync(genAiEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -283,7 +283,7 @@ public async Task ProcessGenAiEventAsync_EmptySummary_NullErrorMessage_LogsUnkno
_sseStream.Setup(s => s.Publish(genAiEvent));
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- GenAiResultListener sut = CreateSut();
+ using GenAiResultListener sut = CreateSut();
// Act
await sut.ProcessGenAiEventAsync(genAiEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -315,7 +315,7 @@ public async Task ProcessGenAiEventAsync_ExceptionThrown_LogsErrorAndNacks()
_consumer.Setup(c => c.NackAsync(It.IsAny())).Returns(Task.CompletedTask);
- GenAiResultListener sut = CreateSut();
+ using GenAiResultListener sut = CreateSut();
// Act
await sut.ProcessGenAiEventAsync(genAiEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -339,7 +339,7 @@ public async Task ProcessGenAiEventAsync_ExceptionThrown_DoesNotCallAck()
_consumer.Setup(c => c.NackAsync(It.IsAny())).Returns(Task.CompletedTask);
- GenAiResultListener sut = CreateSut();
+ using GenAiResultListener sut = CreateSut();
// Act
await sut.ProcessGenAiEventAsync(genAiEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -363,7 +363,7 @@ public async Task ProcessGenAiEventAsync_ExceptionThrown_LogsError()
_consumer.Setup(c => c.NackAsync(It.IsAny())).Returns(Task.CompletedTask);
- GenAiResultListener sut = CreateSut();
+ using GenAiResultListener sut = CreateSut();
// Act
await sut.ProcessGenAiEventAsync(genAiEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -396,7 +396,7 @@ public async Task ProcessGenAiEventAsync_ReceivesEvent_LogsDocumentId()
_sseStream.Setup(s => s.Publish(genAiEvent));
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- GenAiResultListener sut = CreateSut();
+ using GenAiResultListener sut = CreateSut();
// Act
await sut.ProcessGenAiEventAsync(genAiEvent, _consumer.Object, TestContext.Current.CancellationToken);
diff --git a/PaperlessREST.Tests/Unit/GlobalExceptionHandlerTests.cs b/PaperlessREST.Tests/Unit/GlobalExceptionHandlerTests.cs
index b1f1e76..41b0f36 100644
--- a/PaperlessREST.Tests/Unit/GlobalExceptionHandlerTests.cs
+++ b/PaperlessREST.Tests/Unit/GlobalExceptionHandlerTests.cs
@@ -317,7 +317,8 @@ public async Task TryHandleAsync_LogsWithCorrectLevelAndCode()
public async Task TryHandleAsync_UsesActivityIdWhenAvailable()
{
// Arrange
- using Activity activity = new Activity(TestActivityName).Start();
+ using var activity = new Activity(TestActivityName);
+ activity.Start();
DefaultHttpContext httpContext = CreateHttpContext();
SetupProblemDetailsService();
GlobalExceptionHandler sut = CreateSut();
@@ -405,7 +406,7 @@ public sealed class ProblemDetailsEnricherTests : IDisposable
private const string ExpectedTimestamp = "2024-11-29T12:00:00.0000000+00:00";
private const string GenericInternalErrorSubstring = "internal error";
- private static readonly DateTimeOffset FixedTime = new(2024, 11, 29, 12, 0, 0, TimeSpan.Zero);
+ private static readonly DateTimeOffset s_fixedTime = new(2024, 11, 29, 12, 0, 0, TimeSpan.Zero);
private readonly Mock _hostEnvironment;
private readonly MockRepository _mocks = new(MockBehavior.Strict) { DefaultValue = DefaultValue.Empty };
@@ -415,7 +416,7 @@ public ProblemDetailsEnricherTests()
{
_hostEnvironment = _mocks.Create();
_timeProvider = _mocks.Create();
- _timeProvider.Setup(t => t.GetUtcNow()).Returns(FixedTime);
+ _timeProvider.Setup(t => t.GetUtcNow()).Returns(s_fixedTime);
// Default environment - tests override when needed
_hostEnvironment.Setup(e => e.EnvironmentName).Returns(Environments.Production);
}
@@ -430,7 +431,8 @@ public void Dispose()
public void Enrich_WithActivity_UsesActivityId()
{
// Arrange
- using Activity activity = new Activity(TestActivityName).Start();
+ using var activity = new Activity(TestActivityName);
+ activity.Start();
(ProblemDetailsEnricher sut, ProblemDetailsContext context) = CreateSutAndContext();
// Act
diff --git a/PaperlessREST.Tests/Unit/MappingTests.cs b/PaperlessREST.Tests/Unit/MappingTests.cs
index 2449658..bfbaa99 100644
--- a/PaperlessREST.Tests/Unit/MappingTests.cs
+++ b/PaperlessREST.Tests/Unit/MappingTests.cs
@@ -31,7 +31,7 @@ public void DocumentEntity_ToDocument_MapsAllProperties()
// Arrange
DocumentEntity entity = DocumentBuilder.Completed(TestOcrContent).WithId(Guid.CreateVersion7())
.WithFileName(TestFileName)
- .WithSummary(TestSummary, DateTimeOffset.UtcNow.AddHours(-1)).BuildEntity();
+ .WithSummary(TestSummary, TimeProvider.System.GetUtcNow().AddHours(-1)).BuildEntity();
// Act
Document document = entity.ToDocument();
diff --git a/PaperlessREST.Tests/Unit/OcrResultListenerTests.cs b/PaperlessREST.Tests/Unit/OcrResultListenerTests.cs
index 62a32a7..475e44e 100644
--- a/PaperlessREST.Tests/Unit/OcrResultListenerTests.cs
+++ b/PaperlessREST.Tests/Unit/OcrResultListenerTests.cs
@@ -69,7 +69,7 @@ private static OcrEvent CreateOcrEvent(
Guid? jobId = null,
string status = CompletedStatus,
string? text = ExtractedContent) =>
- new(jobId ?? Guid.CreateVersion7(), status, text, DateTimeOffset.UtcNow);
+ new(jobId ?? Guid.CreateVersion7(), status, text, TimeProvider.System.GetUtcNow());
// ═══════════════════════════════════════════════════════════════
// TESTS: ProcessMessage - Completed Status Success Path
@@ -91,7 +91,7 @@ public async Task ProcessMessage_CompletedStatus_ProcessesAndPublishesToSse()
_sseStream.Setup(s => s.Publish(ocrEvent));
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- OcrResultListener sut = CreateSut();
+ using OcrResultListener sut = CreateSut();
// Act
await sut.ProcessMessage(ocrEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -115,7 +115,7 @@ public async Task ProcessMessage_CompletedStatus_LogsSuccessfulProcessing()
_sseStream.Setup(s => s.Publish(ocrEvent));
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- OcrResultListener sut = CreateSut();
+ using OcrResultListener sut = CreateSut();
// Act
await sut.ProcessMessage(ocrEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -147,7 +147,7 @@ public async Task ProcessMessage_FailedStatus_PassesNullContentToService()
_sseStream.Setup(s => s.Publish(ocrEvent));
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- OcrResultListener sut = CreateSut();
+ using OcrResultListener sut = CreateSut();
// Act
await sut.ProcessMessage(ocrEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -184,7 +184,7 @@ public async Task ProcessMessage_NonCompletedStatus_PassesNullContent(string sta
_sseStream.Setup(s => s.Publish(ocrEvent));
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- OcrResultListener sut = CreateSut();
+ using OcrResultListener sut = CreateSut();
// Act
await sut.ProcessMessage(ocrEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -208,7 +208,7 @@ public async Task ProcessMessage_CompletedStatusWithNullText_PassesNullContent()
_sseStream.Setup(s => s.Publish(ocrEvent));
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- OcrResultListener sut = CreateSut();
+ using OcrResultListener sut = CreateSut();
// Act
await sut.ProcessMessage(ocrEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -235,7 +235,7 @@ public async Task ProcessMessage_ProcessingReturnsError_CallsNackWithoutRequeue(
_consumer.Setup(c => c.NackAsync(false)).Returns(Task.CompletedTask);
- OcrResultListener sut = CreateSut();
+ using OcrResultListener sut = CreateSut();
// Act
await sut.ProcessMessage(ocrEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -258,7 +258,7 @@ public async Task ProcessMessage_ProcessingReturnsError_DoesNotPublishToSse()
_consumer.Setup(c => c.NackAsync(false)).Returns(Task.CompletedTask);
- OcrResultListener sut = CreateSut();
+ using OcrResultListener sut = CreateSut();
// Act
await sut.ProcessMessage(ocrEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -287,7 +287,7 @@ public async Task ProcessMessage_ExceptionThrown_CallsNackWithRequeue()
_consumer.Setup(c => c.NackAsync(It.IsAny())).Returns(Task.CompletedTask);
- OcrResultListener sut = CreateSut();
+ using OcrResultListener sut = CreateSut();
// Act
await sut.ProcessMessage(ocrEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -312,7 +312,7 @@ public async Task ProcessMessage_ExceptionThrown_LogsError()
_consumer.Setup(c => c.NackAsync(It.IsAny())).Returns(Task.CompletedTask);
- OcrResultListener sut = CreateSut();
+ using OcrResultListener sut = CreateSut();
// Act
await sut.ProcessMessage(ocrEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -339,7 +339,7 @@ public async Task ProcessMessage_ExceptionThrown_DoesNotCallAck()
_consumer.Setup(c => c.NackAsync(It.IsAny())).Returns(Task.CompletedTask);
- OcrResultListener sut = CreateSut();
+ using OcrResultListener sut = CreateSut();
// Act
await sut.ProcessMessage(ocrEvent, _consumer.Object, TestContext.Current.CancellationToken);
@@ -369,7 +369,7 @@ public async Task ProcessMessage_ReceivesEvent_LogsJobIdAndStatus()
_sseStream.Setup(s => s.Publish(ocrEvent));
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- OcrResultListener sut = CreateSut();
+ using OcrResultListener sut = CreateSut();
// Act
await sut.ProcessMessage(ocrEvent, _consumer.Object, TestContext.Current.CancellationToken);
diff --git a/PaperlessREST.Tests/Unit/ReportProcessorTests.cs b/PaperlessREST.Tests/Unit/ReportProcessorTests.cs
index 44e37cd..08d5754 100644
--- a/PaperlessREST.Tests/Unit/ReportProcessorTests.cs
+++ b/PaperlessREST.Tests/Unit/ReportProcessorTests.cs
@@ -49,7 +49,7 @@ public sealed class ReportProcessorTests : IDisposable
""";
- private static readonly DateOnly ValidDateOnly = new(2024, 1, 15);
+ private static readonly DateOnly s_validDateOnly = new(2024, 1, 15);
private readonly MockFileSystem _fileSystem = new();
private readonly FakeLogCollector _logCollector = new();
private readonly FakeLogger _logger;
@@ -446,7 +446,7 @@ public async Task ProcessAsync_ValidDocuments_CallsRepositoryWithCorrectDate()
.ReturnsAsync([docId]);
_repo.Setup(r => r.UpsertDailyAccessAsync(
- ValidDateOnly,
+ s_validDateOnly,
It.IsAny<(Guid DocumentId, long AccessCount)[]>(),
It.IsAny()))
.Returns(Task.CompletedTask);
@@ -459,7 +459,7 @@ public async Task ProcessAsync_ValidDocuments_CallsRepositoryWithCorrectDate()
// Assert
_repo.Verify(
r => r.UpsertDailyAccessAsync(
- ValidDateOnly,
+ s_validDateOnly,
It.IsAny<(Guid, long)[]>(),
It.IsAny()),
Times.Once);
diff --git a/PaperlessREST/Configuration/ElasticsearchOptions.cs b/PaperlessREST/Configuration/ElasticsearchOptions.cs
index 877166c..9586b09 100644
--- a/PaperlessREST/Configuration/ElasticsearchOptions.cs
+++ b/PaperlessREST/Configuration/ElasticsearchOptions.cs
@@ -11,17 +11,3 @@ public sealed record ElasticsearchOptions
[Required(ErrorMessage = $"{SectionName}:DefaultIndex is required")]
public required string DefaultIndex { get; init; }
}
-
-public static class ElasticsearchOptionsExtensions
-{
- extension(ElasticsearchOptions opts)
- {
- ///
- /// Creates a configured ElasticsearchClient instance.
- ///
- public ElasticsearchClient CreateClient() =>
- new(new ElasticsearchClientSettings(opts.Uri)
- .DefaultIndex(opts.DefaultIndex)
- .ThrowExceptions());
- }
-}
diff --git a/PaperlessREST/Configuration/MinioOptions.cs b/PaperlessREST/Configuration/MinioOptions.cs
index 47bd494..6c321eb 100644
--- a/PaperlessREST/Configuration/MinioOptions.cs
+++ b/PaperlessREST/Configuration/MinioOptions.cs
@@ -25,27 +25,17 @@ public static class MinioOptionsExtensions
extension(MinioOptions opts)
{
///
- /// Parses the Endpoint string into a Uri, adding http(s):// scheme if missing.
+ /// Parses the Endpoint string into a Uri, adding http(s):// scheme if missing.
///
public Uri EndpointUri
{
get
{
- var endpoint = opts.Endpoint.Contains("://")
+ string endpoint = opts.Endpoint.Contains("://", StringComparison.Ordinal)
? opts.Endpoint
: $"{(opts.UseSsl ? "https" : "http")}://{opts.Endpoint}";
return new Uri(endpoint);
}
}
-
- ///
- /// Creates a configured MinioClient instance.
- ///
- public IMinioClient CreateClient() =>
- new MinioClient()
- .WithEndpoint(opts.EndpointUri.Host, opts.EndpointUri.Port)
- .WithCredentials(opts.AccessKey, opts.SecretKey)
- .WithSSL(opts.UseSsl)
- .Build();
}
}
diff --git a/PaperlessREST/Features/BatchProcessing/Application/ReportProcessor.cs b/PaperlessREST/Features/BatchProcessing/Application/ReportProcessor.cs
index 455989f..32dcafb 100644
--- a/PaperlessREST/Features/BatchProcessing/Application/ReportProcessor.cs
+++ b/PaperlessREST/Features/BatchProcessing/Application/ReportProcessor.cs
@@ -57,7 +57,8 @@ private XmlSchemaSet LoadSchemas()
string schemaPath = fs.Path.Combine(AppContext.BaseDirectory, "Schemas", SchemaFileName);
using Stream schemaStream = fs.FileStream.New(schemaPath, FileMode.Open, FileAccess.Read, FileShare.Read);
- schemas.Add("", XmlReader.Create(schemaStream));
+ using XmlReader schemaReader = XmlReader.Create(schemaStream);
+ schemas.Add("", schemaReader);
schemas.Compile();
return schemas;
}
diff --git a/PaperlessREST/Features/DocumentManagement/Presentation/Endpoints/DocumentEndpoints.cs b/PaperlessREST/Features/DocumentManagement/Presentation/Endpoints/DocumentEndpoints.cs
index 24dc1f5..b0bd226 100644
--- a/PaperlessREST/Features/DocumentManagement/Presentation/Endpoints/DocumentEndpoints.cs
+++ b/PaperlessREST/Features/DocumentManagement/Presentation/Endpoints/DocumentEndpoints.cs
@@ -162,12 +162,14 @@ public static async Task>> SearchDocuments(
/// Document with the specified ID does not exist.
/// Domain error: DocumentErrors.NotFound
///
- public static Task, NotFound>> GetDocumentById(
+ public static async Task, NotFound>> GetDocumentById(
Guid id,
IDocumentService documentService,
- CancellationToken cancellationToken) =>
- documentService.GetDocumentByIdAsync(id, cancellationToken)
- .ToOkOr404(doc => doc.ToDocumentDto());
+ CancellationToken cancellationToken)
+ {
+ ErrorOr result = await documentService.GetDocumentByIdAsync(id, cancellationToken);
+ return result.ToOkOr404(doc => doc.ToDocumentDto());
+ }
///
/// Retrieves the AI-generated summary for a document.
@@ -185,12 +187,14 @@ public static Task, NotFound>> GetDocumentById(
/// Document with the specified ID does not exist.
/// Domain error: DocumentErrors.NotFound
///
- public static Task, NotFound>> GetSummary(
+ public static async Task, NotFound>> GetSummary(
Guid id,
IDocumentService documentService,
- CancellationToken cancellationToken) =>
- documentService.GetDocumentByIdAsync(id, cancellationToken)
- .ToOkOr404(doc => new SummaryDto { Summary = doc.Summary });
+ CancellationToken cancellationToken)
+ {
+ ErrorOr result = await documentService.GetDocumentByIdAsync(id, cancellationToken);
+ return result.ToOkOr404(doc => new SummaryDto { Summary = doc.Summary });
+ }
// ═══════════════════════════════════════════════════════════════════════════
// Commands - use ToAcceptedAtRouteOrProblem / ToNoContentOr404
diff --git a/PaperlessREST/Host/Extensions/RichProblemDetailsFactory.cs b/PaperlessREST/Host/Extensions/RichProblemDetailsFactory.cs
index 217d55a..c2a339e 100644
--- a/PaperlessREST/Host/Extensions/RichProblemDetailsFactory.cs
+++ b/PaperlessREST/Host/Extensions/RichProblemDetailsFactory.cs
@@ -174,7 +174,11 @@ public static Error InvalidField(string fieldName, string reason, object? attemp
///
public static Error DocumentNotFound(Guid id, string? suggestion = null)
{
- Dictionary metadata = new() { ["DocumentId"] = id, ["SearchedAt"] = DateTimeOffset.UtcNow };
+ Dictionary metadata = new()
+ {
+ ["DocumentId"] = id,
+ ["SearchedAt"] = TimeProvider.System.GetUtcNow()
+ };
if (suggestion is not null)
{
diff --git a/PaperlessREST/Host/Extensions/ServiceCollectionExtensions.cs b/PaperlessREST/Host/Extensions/ServiceCollectionExtensions.cs
index 7f57ab0..791d34a 100644
--- a/PaperlessREST/Host/Extensions/ServiceCollectionExtensions.cs
+++ b/PaperlessREST/Host/Extensions/ServiceCollectionExtensions.cs
@@ -240,9 +240,15 @@ private IServiceCollection AddObjectStorage()
.BindConfiguration(MinioOptions.SectionName)
.ValidateDataAnnotations();
- // Use extension method from MinioOptionsExtensions
services.AddSingleton(sp =>
- sp.GetRequiredService>().Value.CreateClient());
+ {
+ MinioOptions opts = sp.GetRequiredService>().Value;
+ return new MinioClient()
+ .WithEndpoint(opts.EndpointUri.Host, opts.EndpointUri.Port)
+ .WithCredentials(opts.AccessKey, opts.SecretKey)
+ .WithSSL(opts.UseSsl)
+ .Build();
+ });
return services;
}
@@ -254,7 +260,13 @@ private IServiceCollection AddSearchEngine()
.ValidateDataAnnotations();
services.AddSingleton(sp =>
- sp.GetRequiredService>().Value.CreateClient());
+ {
+ ElasticsearchOptions opts = sp.GetRequiredService>().Value;
+ return new ElasticsearchClient(
+ new ElasticsearchClientSettings(opts.Uri)
+ .DefaultIndex(opts.DefaultIndex)
+ .ThrowExceptions());
+ });
return services;
}
diff --git a/PaperlessREST/Host/Extensions/TypedErrorOrAsyncExtensions.cs b/PaperlessREST/Host/Extensions/TypedErrorOrAsyncExtensions.cs
index b3705ae..c8859ba 100644
--- a/PaperlessREST/Host/Extensions/TypedErrorOrAsyncExtensions.cs
+++ b/PaperlessREST/Host/Extensions/TypedErrorOrAsyncExtensions.cs
@@ -58,18 +58,16 @@ private static string ToKebabCase(string value) =>
// ValueTask> Extensions
// ═══════════════════════════════════════════════════════════════════════════
- extension(ValueTask> task)
+ extension(ErrorOr result)
{
///
/// Converts to (200) or (404).
///
[MustUseReturnValue]
- public async Task, NotFound>> ToOkOr404(
+ public Results, NotFound> ToOkOr404(
[InstantHandle] Func mapper,
[CallerMemberName] string callerName = "")
{
- ErrorOr result = await task.ConfigureAwait(false);
-
if (!result.IsError)
{
return TypedResults.Ok(mapper(result.Value));
@@ -79,6 +77,40 @@ public async Task, NotFound>> ToOkOr404(
? TypedResults.NotFound()
: throw ContractViolationException.ForNotFoundOnly(result.FirstError, result.Errors, callerName);
}
+ }
+
+ extension(ErrorOr result)
+ {
+ ///
+ /// Converts to (204) or (404).
+ ///
+ [MustUseReturnValue]
+ public Results ToNoContentOr404([CallerMemberName] string callerName = "")
+ {
+ if (!result.IsError)
+ {
+ return TypedResults.NoContent();
+ }
+
+ return result.FirstError.Type == ErrorType.NotFound
+ ? TypedResults.NotFound()
+ : throw ContractViolationException.ForNotFoundOnly(result.FirstError, result.Errors, callerName);
+ }
+ }
+
+ extension(ValueTask> task)
+ {
+ ///
+ /// Converts to (200) or (404).
+ ///
+ [MustUseReturnValue]
+ public async Task, NotFound>> ToOkOr404(
+ [InstantHandle] Func mapper,
+ [CallerMemberName] string callerName = "")
+ {
+ ErrorOr result = await task.ConfigureAwait(false);
+ return result.ToOkOr404(mapper, callerName);
+ }
///
/// Converts to (202),
@@ -120,15 +152,7 @@ public async Task, ValidationProblem, ProblemHt
public async Task> ToNoContentOr404([CallerMemberName] string callerName = "")
{
ErrorOr result = await task.ConfigureAwait(false);
-
- if (!result.IsError)
- {
- return TypedResults.NoContent();
- }
-
- return result.FirstError.Type == ErrorType.NotFound
- ? TypedResults.NotFound()
- : throw ContractViolationException.ForNotFoundOnly(result.FirstError, result.Errors, callerName);
+ return result.ToNoContentOr404(callerName);
}
}
diff --git a/PaperlessServices.Tests/GlobalUsings.cs b/PaperlessServices.Tests/GlobalUsings.cs
index d969c52..6dd0eb5 100644
--- a/PaperlessServices.Tests/GlobalUsings.cs
+++ b/PaperlessServices.Tests/GlobalUsings.cs
@@ -1,5 +1,6 @@
// System namespaces
+global using System.Diagnostics.CodeAnalysis;
global using System.Text.Json;
// Microsoft namespaces
diff --git a/PaperlessServices.Tests/Integration/FakeTextSummarizer.cs b/PaperlessServices.Tests/Integration/FakeTextSummarizer.cs
index fccaf4b..341012e 100644
--- a/PaperlessServices.Tests/Integration/FakeTextSummarizer.cs
+++ b/PaperlessServices.Tests/Integration/FakeTextSummarizer.cs
@@ -6,7 +6,6 @@ internal sealed class FakeTextSummarizer : ITextSummarizer
public Task SummarizeAsync(string text, CancellationToken cancellationToken = default)
{
- ArgumentException.ThrowIfNullOrWhiteSpace(text);
cancellationToken.ThrowIfCancellationRequested();
string preview = text.Length <= 64 ? text : text[..64] + "…";
diff --git a/PaperlessServices.Tests/Integration/OcrIntegrationTests.cs b/PaperlessServices.Tests/Integration/OcrIntegrationTests.cs
index 3eac3d3..5a9974e 100644
--- a/PaperlessServices.Tests/Integration/OcrIntegrationTests.cs
+++ b/PaperlessServices.Tests/Integration/OcrIntegrationTests.cs
@@ -10,7 +10,7 @@ public async Task ExtractsInvoiceNumber()
{
// Arrange
string storagePath = await fixture.UploadPdfAsync("INV-2024-001");
- OcrCommand command = new(Guid.NewGuid(), "invoice.pdf", storagePath, DateTimeOffset.UtcNow.AddMinutes(-5));
+ OcrCommand command = new(Guid.NewGuid(), "invoice.pdf", storagePath, TimeProvider.System.GetUtcNow().AddMinutes(-5));
// Act
ErrorOr errorOrResult =
@@ -32,7 +32,7 @@ public async Task ProcessMultipleDocuments_Concurrently()
IEnumerable>> tasks = Enumerable.Range(1, 3).Select(async i =>
{
string path = await fixture.UploadPdfAsync($"Document {i} content. Amount: ${i * 1000:N2}");
- OcrCommand command = new(Guid.NewGuid(), $"doc-{i}.pdf", path, DateTimeOffset.UtcNow.AddMinutes(-5));
+ OcrCommand command = new(Guid.NewGuid(), $"doc-{i}.pdf", path, TimeProvider.System.GetUtcNow().AddMinutes(-5));
return await OcrProcessor.ProcessDocumentAsync(command, TestContext.Current.CancellationToken);
});
@@ -55,10 +55,10 @@ public class GenAiIntegrationTests(SharedContainerFixture fixture, ITestOutputHe
[Fact]
public async Task SummarizesFinancialReport()
{
- const string report = "Q3 2024 Report: Revenue $4.2M (+28% YoY), Expenses $2.1M (+12%)";
+ const string Report = "Q3 2024 Report: Revenue $4.2M (+28% YoY), Expenses $2.1M (+12%)";
string? summary = await TextSummarizer.SummarizeAsync(
- report,
+ Report,
TestContext.Current.CancellationToken);
summary.Should().NotBeNullOrWhiteSpace();
diff --git a/PaperlessServices.Tests/Integration/SearchIndexIntegrationTests.cs b/PaperlessServices.Tests/Integration/SearchIndexIntegrationTests.cs
index 146bf3c..99c1c2a 100644
--- a/PaperlessServices.Tests/Integration/SearchIndexIntegrationTests.cs
+++ b/PaperlessServices.Tests/Integration/SearchIndexIntegrationTests.cs
@@ -13,12 +13,12 @@ public async Task IndexDocument_StoresInElasticsearch()
{
// Arrange
Guid id = Guid.NewGuid();
- const string fileName = "test.pdf";
- const string content = "Test content";
- await fixture.UploadPdfAsync(content);
+ const string FileName = "test.pdf";
+ const string Content = "Test content";
+ await fixture.UploadPdfAsync(Content);
// Act
- await SearchIndex.IndexDocumentAsync(id, fileName, content, DateTimeOffset.UtcNow.AddMinutes(-5),
+ await SearchIndex.IndexDocumentAsync(id, FileName, Content, TimeProvider.System.GetUtcNow().AddMinutes(-5),
TestContext.Current.CancellationToken);
// Assert - poll until indexed
@@ -27,8 +27,8 @@ await SearchIndex.IndexDocumentAsync(id, fileName, content, DateTimeOffset.UtcNo
TestContext.Current.CancellationToken);
response.IsSuccess().Should().BeTrue();
- response.Source.GetProperty("fileName").GetString().Should().Be(fileName);
- response.Source.GetProperty("content").GetString().Should().Be(content);
+ response.Source.GetProperty("fileName").GetString().Should().Be(FileName);
+ response.Source.GetProperty("content").GetString().Should().Be(Content);
}
[Fact]
@@ -39,7 +39,7 @@ public async Task HelloWorldPdf_SearchableByHello()
await fixture.UploadPdfAsync("Hello World!");
// Act
- await SearchIndex.IndexDocumentAsync(id, "HelloWorld.pdf", "Hello World!", DateTimeOffset.UtcNow.AddMinutes(-5),
+ await SearchIndex.IndexDocumentAsync(id, "HelloWorld.pdf", "Hello World!", TimeProvider.System.GetUtcNow().AddMinutes(-5),
TestContext.Current.CancellationToken);
// Assert - poll until searchable
@@ -58,7 +58,7 @@ public async Task OcrProcessor_IndexesDocument()
// Arrange
Guid jobId = Guid.NewGuid();
string storagePath = await fixture.UploadPdfAsync("Hello World!");
- OcrCommand command = new(jobId, "HelloWorld.pdf", storagePath, DateTimeOffset.UtcNow.AddMinutes(-5));
+ OcrCommand command = new(jobId, "HelloWorld.pdf", storagePath, TimeProvider.System.GetUtcNow().AddMinutes(-5));
// Act
ErrorOr errorOrResult = await OcrProcessor.ProcessDocumentAsync(command,
@@ -95,11 +95,11 @@ public async Task MultipleDocuments_SearchCorrectly()
Guid testId = Guid.NewGuid();
await SearchIndex.IndexDocumentAsync(helloId, "HelloWorld.pdf", "Hello World",
- DateTimeOffset.UtcNow.AddMinutes(-5),
+ TimeProvider.System.GetUtcNow().AddMinutes(-5),
TestContext.Current.CancellationToken);
await SearchIndex.IndexDocumentAsync(testId, "TestDoc.pdf", "Test document",
- DateTimeOffset.UtcNow.AddMinutes(-5),
+ TimeProvider.System.GetUtcNow().AddMinutes(-5),
TestContext.Current.CancellationToken);
// Act & Assert - Hello document
@@ -135,7 +135,7 @@ public async Task IndexDocument_LogsActivity()
await fixture.UploadPdfAsync("Test");
// Act
- await SearchIndex.IndexDocumentAsync(id, "test.pdf", "Test", DateTimeOffset.UtcNow.AddMinutes(-5),
+ await SearchIndex.IndexDocumentAsync(id, "test.pdf", "Test", TimeProvider.System.GetUtcNow().AddMinutes(-5),
TestContext.Current.CancellationToken);
// Assert
diff --git a/PaperlessServices.Tests/Integration/WorkerTestBase.cs b/PaperlessServices.Tests/Integration/WorkerTestBase.cs
index 27e0d74..4de2ea7 100644
--- a/PaperlessServices.Tests/Integration/WorkerTestBase.cs
+++ b/PaperlessServices.Tests/Integration/WorkerTestBase.cs
@@ -67,11 +67,11 @@ await Task.WhenAll(
string minioEndpoint = $"{_minio.Hostname}:{_minio.GetMappedPublicPort(MinioPort)}";
- IMinioClient minioClient = new MinioClient()
+ using MinioClient minioClient = new();
+ minioClient
.WithEndpoint(minioEndpoint)
.WithCredentials(_minio.GetAccessKey(), _minio.GetSecretKey())
.Build();
-
await minioClient.MakeBucketAsync(new MakeBucketArgs().WithBucket(_bucketName));
HostApplicationBuilder builder = Microsoft.Extensions.Hosting.Host.CreateApplicationBuilder();
@@ -125,7 +125,7 @@ public async Task UploadPdfAsync(string content)
string fileName = $"test-{Guid.NewGuid():N}.pdf";
string pdfPath = await Pdf.Create(Dye.White).AddText(content).SaveAsync(fileName);
- string storageKey = $"documents/{DateTime.UtcNow:yyyy-MM}/{Guid.NewGuid():N}/{fileName}";
+ string storageKey = $"documents/{TimeProvider.System.GetUtcNow():yyyy-MM}/{Guid.NewGuid():N}/{fileName}";
IMinioClient client = Services.GetRequiredService();
await using FileStream stream = File.OpenRead(pdfPath);
diff --git a/PaperlessServices.Tests/Unit/OcrProcessorTests.cs b/PaperlessServices.Tests/Unit/OcrProcessorTests.cs
index ca4d7e0..1d61b69 100644
--- a/PaperlessServices.Tests/Unit/OcrProcessorTests.cs
+++ b/PaperlessServices.Tests/Unit/OcrProcessorTests.cs
@@ -9,7 +9,7 @@ public sealed class OcrProcessorTests : IDisposable
private const string ValidFileName = "document.pdf";
private const string ValidStoragePath = "documents/2024-01/abc123.pdf";
private const string ExtractedOcrText = "This is the extracted text from the PDF document.";
- private static readonly Guid TestJobId = Guid.Parse("12345678-1234-1234-1234-123456789abc");
+ private static readonly Guid s_testJobId = Guid.Parse("12345678-1234-1234-1234-123456789abc");
private readonly FakeLogCollector _logCollector = new();
private readonly FakeLogger _logger;
@@ -50,7 +50,7 @@ private static OcrCommand CreateCommand(
string fileName = ValidFileName,
string storagePath = ValidStoragePath,
DateTimeOffset? createdAt = null) =>
- new(jobId ?? TestJobId, fileName, storagePath, createdAt ?? DateTimeOffset.UtcNow.AddMinutes(-5));
+ new(jobId ?? s_testJobId, fileName, storagePath, createdAt ?? TimeProvider.System.GetUtcNow().AddMinutes(-5));
private static MemoryStream CreateValidPdfStream() => new([0x25, 0x50, 0x44, 0x46]); // %PDF header
@@ -69,7 +69,7 @@ public async Task ProcessDocumentAsync_ValidCommand_DownloadsFromStorage()
_pdfExtractor.Setup(p => p.ExtractTextAsync(It.IsAny()))
.ReturnsAsync(ExtractedOcrText);
_searchIndex.Setup(i => i.IndexDocumentAsync(
- TestJobId, ValidFileName, ExtractedOcrText, It.IsAny(), It.IsAny()))
+ s_testJobId, ValidFileName, ExtractedOcrText, It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
OcrProcessor sut = CreateSut();
@@ -91,7 +91,7 @@ public async Task ProcessDocumentAsync_ValidCommand_ExtractsTextWithOcr()
_pdfExtractor.Setup(p => p.ExtractTextAsync(It.IsAny()))
.ReturnsAsync(ExtractedOcrText);
_searchIndex.Setup(i => i.IndexDocumentAsync(
- TestJobId, ValidFileName, ExtractedOcrText, It.IsAny(), It.IsAny()))
+ s_testJobId, ValidFileName, ExtractedOcrText, It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
OcrProcessor sut = CreateSut();
@@ -113,7 +113,7 @@ public async Task ProcessDocumentAsync_ValidCommand_IndexesDocumentInSearch()
_pdfExtractor.Setup(p => p.ExtractTextAsync(It.IsAny()))
.ReturnsAsync(ExtractedOcrText);
_searchIndex.Setup(i => i.IndexDocumentAsync(
- TestJobId, ValidFileName, ExtractedOcrText, It.IsAny(), It.IsAny()))
+ s_testJobId, ValidFileName, ExtractedOcrText, It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
OcrProcessor sut = CreateSut();
@@ -135,7 +135,7 @@ public async Task ProcessDocumentAsync_ValidCommand_ReturnsCompletedOcrEvent()
_pdfExtractor.Setup(p => p.ExtractTextAsync(It.IsAny()))
.ReturnsAsync(ExtractedOcrText);
_searchIndex.Setup(i => i.IndexDocumentAsync(
- TestJobId, ValidFileName, ExtractedOcrText, It.IsAny(), It.IsAny()))
+ s_testJobId, ValidFileName, ExtractedOcrText, It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
OcrProcessor sut = CreateSut();
@@ -146,7 +146,7 @@ public async Task ProcessDocumentAsync_ValidCommand_ReturnsCompletedOcrEvent()
// Assert
result.IsError.Should().BeFalse();
- result.Value.JobId.Should().Be(TestJobId);
+ result.Value.JobId.Should().Be(s_testJobId);
result.Value.Status.Should().Be("Completed");
result.Value.Text.Should().Be(ExtractedOcrText);
}
@@ -166,7 +166,7 @@ public async Task ProcessDocumentAsync_Success_LogsInformation()
_pdfExtractor.Setup(p => p.ExtractTextAsync(It.IsAny()))
.ReturnsAsync(ExtractedOcrText);
_searchIndex.Setup(i => i.IndexDocumentAsync(
- TestJobId, ValidFileName, ExtractedOcrText, It.IsAny(), It.IsAny()))
+ s_testJobId, ValidFileName, ExtractedOcrText, It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
OcrProcessor sut = CreateSut();
@@ -333,7 +333,7 @@ public async Task ProcessDocumentAsync_EmptyExtractedText_StillReturnsSuccess()
_pdfExtractor.Setup(p => p.ExtractTextAsync(It.IsAny()))
.ReturnsAsync(string.Empty);
_searchIndex.Setup(i => i.IndexDocumentAsync(
- TestJobId, ValidFileName, string.Empty, It.IsAny(), It.IsAny()))
+ s_testJobId, ValidFileName, string.Empty, It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
OcrProcessor sut = CreateSut();
@@ -360,7 +360,7 @@ public async Task ProcessDocumentAsync_LargeExtractedText_HandlesCorrectly()
_pdfExtractor.Setup(p => p.ExtractTextAsync(It.IsAny()))
.ReturnsAsync(largeText);
_searchIndex.Setup(i => i.IndexDocumentAsync(
- TestJobId, ValidFileName, largeText, It.IsAny(), It.IsAny()))
+ s_testJobId, ValidFileName, largeText, It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
OcrProcessor sut = CreateSut();
diff --git a/PaperlessServices.Tests/Unit/OcrWorkerTests.cs b/PaperlessServices.Tests/Unit/OcrWorkerTests.cs
index 90888f4..619defd 100644
--- a/PaperlessServices.Tests/Unit/OcrWorkerTests.cs
+++ b/PaperlessServices.Tests/Unit/OcrWorkerTests.cs
@@ -74,7 +74,7 @@ public async Task SuccessfulOcr_AcknowledgesMessage()
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- OcrWorker sut = CreateSut();
+ using OcrWorker sut = CreateSut();
await sut.ProcessMessage(command, _consumer.Object, TestContext.Current.CancellationToken);
}
@@ -96,7 +96,7 @@ public async Task SuccessfulOcr_LogsCompletion()
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- OcrWorker sut = CreateSut();
+ using OcrWorker sut = CreateSut();
await sut.ProcessMessage(command, _consumer.Object, TestContext.Current.CancellationToken);
@@ -123,7 +123,7 @@ public async Task SuccessfulOcr_LogsProcessingStart()
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- OcrWorker sut = CreateSut();
+ using OcrWorker sut = CreateSut();
await sut.ProcessMessage(command, _consumer.Object, TestContext.Current.CancellationToken);
@@ -148,7 +148,7 @@ public async Task OcrFails_AcknowledgesMessage()
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- OcrWorker sut = CreateSut();
+ using OcrWorker sut = CreateSut();
await sut.ProcessMessage(command, _consumer.Object, TestContext.Current.CancellationToken);
}
@@ -168,7 +168,7 @@ public async Task OcrFails_LogsWarning()
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- OcrWorker sut = CreateSut();
+ using OcrWorker sut = CreateSut();
await sut.ProcessMessage(command, _consumer.Object, TestContext.Current.CancellationToken);
@@ -193,7 +193,7 @@ public async Task OcrFails_LogsErrorCode()
_consumer.Setup(c => c.AckAsync()).Returns(Task.CompletedTask);
- OcrWorker sut = CreateSut();
+ using OcrWorker sut = CreateSut();
await sut.ProcessMessage(command, _consumer.Object, TestContext.Current.CancellationToken);
@@ -215,7 +215,7 @@ public async Task ProcessorThrows_NacksMessage()
_consumer.Setup(c => c.NackAsync()).Returns(Task.CompletedTask);
- OcrWorker sut = CreateSut();
+ using OcrWorker sut = CreateSut();
await sut.ProcessMessage(command, _consumer.Object, TestContext.Current.CancellationToken);
}
@@ -232,7 +232,7 @@ public async Task ProcessorThrows_DoesNotAck()
_consumer.Setup(c => c.NackAsync()).Returns(Task.CompletedTask);
- OcrWorker sut = CreateSut();
+ using OcrWorker sut = CreateSut();
await sut.ProcessMessage(command, _consumer.Object, TestContext.Current.CancellationToken);
@@ -251,7 +251,7 @@ public async Task ProcessorThrows_LogsError()
_consumer.Setup(c => c.NackAsync()).Returns(Task.CompletedTask);
- OcrWorker sut = CreateSut();
+ using OcrWorker sut = CreateSut();
await sut.ProcessMessage(command, _consumer.Object, TestContext.Current.CancellationToken);
@@ -273,7 +273,7 @@ public async Task ProcessorThrows_LogsJobId()
_consumer.Setup(c => c.NackAsync()).Returns(Task.CompletedTask);
- OcrWorker sut = CreateSut();
+ using OcrWorker sut = CreateSut();
await sut.ProcessMessage(command, _consumer.Object, TestContext.Current.CancellationToken);
@@ -367,7 +367,7 @@ public async Task EmptyStream_CompletesGracefully()
_consumer.Setup(c => c.ConsumeAsync(It.IsAny()))
.Returns(CreateAsyncEnumerable());
- OcrWorker sut = CreateSut();
+ using OcrWorker sut = CreateSut();
await sut.StartAsync(cts.Token);
@@ -406,7 +406,7 @@ public async Task ProcessesMultipleMessages()
_ocrProcessor.Setup(p => p.ProcessDocumentAsync(command2, It.IsAny()))
.ReturnsAsync(CreateSuccessEvent(jobId2));
- OcrWorker sut = CreateSut();
+ using OcrWorker sut = CreateSut();
await sut.StartAsync(cts.Token);
@@ -445,7 +445,7 @@ public async Task CancellationStopsProcessing()
return CreateSuccessEvent(cmd.JobId);
});
- OcrWorker sut = CreateSut();
+ using OcrWorker sut = CreateSut();
await sut.StartAsync(cts.Token);
@@ -489,7 +489,7 @@ public async Task ProcessorThrows_ContinuesWithNextMessage()
_ocrProcessor.Setup(p => p.ProcessDocumentAsync(command3, It.IsAny()))
.ReturnsAsync(CreateSuccessEvent(jobId3));
- OcrWorker sut = CreateSut();
+ using OcrWorker sut = CreateSut();
await sut.StartAsync(cts.Token);
@@ -527,10 +527,10 @@ private static OcrCommand CreateCommand(
string fileName = ValidFileName,
string storagePath = ValidStoragePath,
DateTimeOffset? createdAt = null) =>
- new(jobId ?? Guid.CreateVersion7(), fileName, storagePath, createdAt ?? DateTimeOffset.UtcNow.AddMinutes(-5));
+ new(jobId ?? Guid.CreateVersion7(), fileName, storagePath, createdAt ?? TimeProvider.System.GetUtcNow().AddMinutes(-5));
private static OcrEvent CreateSuccessEvent(Guid jobId) =>
- new(jobId, "Completed", ExtractedOcrText, DateTimeOffset.UtcNow);
+ new(jobId, "Completed", ExtractedOcrText, TimeProvider.System.GetUtcNow());
private static async IAsyncEnumerable CreateAsyncEnumerable(params OcrCommand[] commands)
{
diff --git a/PaperlessServices.Tests/Unit/SearchIndexServiceTests.cs b/PaperlessServices.Tests/Unit/SearchIndexServiceTests.cs
index dc6a322..eaee090 100644
--- a/PaperlessServices.Tests/Unit/SearchIndexServiceTests.cs
+++ b/PaperlessServices.Tests/Unit/SearchIndexServiceTests.cs
@@ -21,28 +21,31 @@ public sealed class SearchIndexServiceTests : IDisposable
private const string TestIndexName = "test-documents";
private const string TestFileName = "document.pdf";
private const string TestContent = "This is the extracted OCR content from the document.";
- private static readonly Guid TestDocumentId = Guid.Parse("12345678-1234-1234-1234-123456789abc");
+ private static readonly Guid s_testDocumentId = Guid.Parse("12345678-1234-1234-1234-123456789abc");
// ═══════════════════════════════════════════════════════════════
// CONSTRUCTION
// ═══════════════════════════════════════════════════════════════
private readonly FakeLogCollector _logCollector = new();
+ private readonly ElasticsearchClientSettings _settings;
private readonly SearchIndexService _sut;
private readonly FakeTimeProvider _timeProvider = new();
+ [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope",
+ Justification = "_settings is stored as a field and disposed in Dispose() via the IDisposable cast; "
+ + "the analyzer can't follow disposal through the cast.")]
public SearchIndexServiceTests()
{
FakeLogger logger = new(_logCollector);
// Configure client to fail fast - unreachable endpoint tests resilience
- ElasticsearchClientSettings settings = new ElasticsearchClientSettings(new Uri(UnreachableHost))
+ _settings = new ElasticsearchClientSettings(new Uri(UnreachableHost))
.DefaultIndex(TestIndexName)
.DisableDirectStreaming()
.RequestTimeout(TimeSpan.FromMilliseconds(100))
.ThrowExceptions(false);
-
- ElasticsearchClient client = new(settings);
+ ElasticsearchClient client = new(_settings);
IOptions options = Options.Create(new ElasticsearchOptions
{
@@ -56,8 +59,11 @@ public SearchIndexServiceTests()
// DISPOSAL
// ═══════════════════════════════════════════════════════════════
- public void Dispose() =>
+ public void Dispose()
+ {
TestContext.Current.SendDiagnosticMessage("Full logs:\n{0}", _logCollector.GetFullLoggerText());
+ (_settings as IDisposable)?.Dispose();
+ }
// ═══════════════════════════════════════════════════════════════
// TESTS: IndexDocumentAsync - Resilience (Elasticsearch Unavailable)
@@ -68,10 +74,10 @@ public async Task IndexDocumentAsync_WhenElasticsearchUnavailable_DoesNotThrow()
{
// Arrange & Act
Func act = () => _sut.IndexDocumentAsync(
- TestDocumentId,
+ s_testDocumentId,
TestFileName,
TestContent,
- DateTimeOffset.UtcNow.AddMinutes(-5),
+ TimeProvider.System.GetUtcNow().AddMinutes(-5),
TestContext.Current.CancellationToken);
// Assert - Service is resilient to ES failures, never blocks OCR
@@ -83,10 +89,10 @@ public async Task IndexDocumentAsync_WhenElasticsearchUnavailable_LogsWarning()
{
// Act
await _sut.IndexDocumentAsync(
- TestDocumentId,
+ s_testDocumentId,
TestFileName,
TestContent,
- DateTimeOffset.UtcNow.AddMinutes(-5),
+ TimeProvider.System.GetUtcNow().AddMinutes(-5),
TestContext.Current.CancellationToken);
// Assert - Warning logged for observability
@@ -99,16 +105,16 @@ public async Task IndexDocumentAsync_WhenElasticsearchUnavailable_LogsDocumentId
{
// Act
await _sut.IndexDocumentAsync(
- TestDocumentId,
+ s_testDocumentId,
TestFileName,
TestContent,
- DateTimeOffset.UtcNow.AddMinutes(-5),
+ TimeProvider.System.GetUtcNow().AddMinutes(-5),
TestContext.Current.CancellationToken);
// Assert - Document ID in log for troubleshooting
IReadOnlyList logs = _logCollector.GetSnapshot();
logs.Select(r => r.Message)
- .Should().Contain(m => m.Contains(TestDocumentId.ToString(), StringComparison.OrdinalIgnoreCase));
+ .Should().Contain(m => m.Contains(s_testDocumentId.ToString(), StringComparison.OrdinalIgnoreCase));
}
// ═══════════════════════════════════════════════════════════════
@@ -120,10 +126,10 @@ public async Task IndexDocumentAsync_WithNullFileName_DoesNotThrow()
{
// Act
Func act = () => _sut.IndexDocumentAsync(
- TestDocumentId,
+ s_testDocumentId,
null!,
TestContent,
- DateTimeOffset.UtcNow.AddMinutes(-5),
+ TimeProvider.System.GetUtcNow().AddMinutes(-5),
TestContext.Current.CancellationToken);
// Assert - Service handles null gracefully
@@ -135,10 +141,10 @@ public async Task IndexDocumentAsync_WithEmptyContent_DoesNotThrow()
{
// Act - OCR might extract no text from some PDFs
Func act = () => _sut.IndexDocumentAsync(
- TestDocumentId,
+ s_testDocumentId,
TestFileName,
string.Empty,
- DateTimeOffset.UtcNow.AddMinutes(-5),
+ TimeProvider.System.GetUtcNow().AddMinutes(-5),
TestContext.Current.CancellationToken);
// Assert - Empty content is valid
@@ -153,7 +159,7 @@ public async Task IndexDocumentAsync_WithEmptyGuid_DoesNotThrow()
Guid.Empty,
TestFileName,
TestContent,
- DateTimeOffset.UtcNow.AddMinutes(-5),
+ TimeProvider.System.GetUtcNow().AddMinutes(-5),
TestContext.Current.CancellationToken);
// Assert - Invalid IDs shouldn't crash the service
@@ -173,10 +179,10 @@ public async Task IndexDocumentAsync_WithCancelledToken_DoesNotThrow()
// Act
Func act = () => _sut.IndexDocumentAsync(
- TestDocumentId,
+ s_testDocumentId,
TestFileName,
TestContent,
- DateTimeOffset.UtcNow.AddMinutes(-5),
+ TimeProvider.System.GetUtcNow().AddMinutes(-5),
cts.Token);
// Assert - Cancelled operations handled gracefully
@@ -208,7 +214,7 @@ public async Task IndexDocumentAsync_WithVariousContentTypes_DoesNotThrow(string
Guid.CreateVersion7(),
TestFileName,
content,
- DateTimeOffset.UtcNow.AddMinutes(-5),
+ TimeProvider.System.GetUtcNow().AddMinutes(-5),
TestContext.Current.CancellationToken);
// Assert
@@ -226,7 +232,7 @@ public async Task IndexDocumentAsync_WithVeryLongContent_DoesNotThrow()
Guid.CreateVersion7(),
TestFileName,
longContent,
- DateTimeOffset.UtcNow.AddMinutes(-5),
+ TimeProvider.System.GetUtcNow().AddMinutes(-5),
TestContext.Current.CancellationToken);
// Assert
@@ -241,7 +247,7 @@ public async Task IndexDocumentAsync_WithVeryLongContent_DoesNotThrow()
public async Task IndexDocumentAsync_ConcurrentCalls_DoNotThrow()
{
// Arrange - Multiple documents indexed concurrently
- DateTimeOffset createdAt = DateTimeOffset.UtcNow.AddMinutes(-5);
+ DateTimeOffset createdAt = TimeProvider.System.GetUtcNow().AddMinutes(-5);
Task[] tasks =
[
_sut.IndexDocumentAsync(Guid.CreateVersion7(), "doc1.pdf", "Content 1", createdAt, CancellationToken.None),
@@ -264,25 +270,25 @@ public async Task IndexDocumentAsync_ConcurrentCalls_DoNotThrow()
public void LogIndexResult_WhenValid_LogsInformation()
{
// Act
- _sut.LogIndexResult(TestDocumentId, true);
+ _sut.LogIndexResult(s_testDocumentId, true);
// Assert
IReadOnlyList logs = _logCollector.GetSnapshot();
logs.Should().ContainSingle(r =>
r.Level == LogLevel.Information &&
- r.Message.Contains(TestDocumentId.ToString(), StringComparison.OrdinalIgnoreCase));
+ r.Message.Contains(s_testDocumentId.ToString(), StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void LogIndexResult_WhenInvalid_LogsWarning()
{
// Act
- _sut.LogIndexResult(TestDocumentId, false);
+ _sut.LogIndexResult(s_testDocumentId, false);
// Assert
IReadOnlyList logs = _logCollector.GetSnapshot();
logs.Should().ContainSingle(r =>
r.Level == LogLevel.Warning &&
- r.Message.Contains(TestDocumentId.ToString(), StringComparison.OrdinalIgnoreCase));
+ r.Message.Contains(s_testDocumentId.ToString(), StringComparison.OrdinalIgnoreCase));
}
}
diff --git a/PaperlessUI.Blazor/Components/Pages/Weather.razor b/PaperlessUI.Blazor/Components/Pages/Weather.razor
index f437e5e..0062694 100644
--- a/PaperlessUI.Blazor/Components/Pages/Weather.razor
+++ b/PaperlessUI.Blazor/Components/Pages/Weather.razor
@@ -1,5 +1,6 @@
@page "/weather"
@attribute [StreamRendering]
+@using System.Security.Cryptography
Weather
@@ -44,13 +45,13 @@ else
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
- var startDate = DateOnly.FromDateTime(DateTime.Now);
+ var startDate = DateOnly.FromDateTime(TimeProvider.System.GetUtcNow().LocalDateTime);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
- TemperatureC = Random.Shared.Next(-20, 55),
- Summary = summaries[Random.Shared.Next(summaries.Length)]
+ TemperatureC = RandomNumberGenerator.GetInt32(-20, 55),
+ Summary = summaries[RandomNumberGenerator.GetInt32(summaries.Length)]
}).ToArray();
}
diff --git a/Pipeline/Components/ITest.cs b/Pipeline/Components/ITest.cs
index 9233bd8..806131b 100644
--- a/Pipeline/Components/ITest.cs
+++ b/Pipeline/Components/ITest.cs
@@ -42,7 +42,7 @@ internal interface ITest : ICompile
.Description("Run unit tests only")
.DependsOn(x => x.Compile)
.Executes(() => RunTests(new TestOptions(
- NamespaceFilter: "*.Unit.*",
+ NamespaceFilter: "*.Unit",
ReportPrefix: "Unit")));
Target IntegrationTests => d => d
@@ -50,7 +50,7 @@ internal interface ITest : ICompile
.DependsOn(x => x.Compile)
.TryDependsOn()
.Executes(() => RunTests(new TestOptions(
- NamespaceFilter: "*.Integration.*",
+ NamespaceFilter: "*.Integration",
ReportPrefix: "Integration")));
// ══════════════════════════════════════════════════════════════════════════
diff --git a/global.json b/global.json
index 8b810e1..46a26bd 100644
--- a/global.json
+++ b/global.json
@@ -4,9 +4,9 @@
"rollForward": "latestFeature"
},
"msbuild-sdks": {
- "ANcpLua.NET.Sdk": "3.4.27",
- "ANcpLua.NET.Sdk.Web": "3.4.27",
- "ANcpLua.NET.Sdk.Test": "3.4.27"
+ "ANcpLua.NET.Sdk": "3.4.29",
+ "ANcpLua.NET.Sdk.Web": "3.4.29",
+ "ANcpLua.NET.Sdk.Test": "3.4.29"
},
"test": {
"runner": "Microsoft.Testing.Platform"