Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .nuke/build.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"Restore",
"SetupTestcontainers",
"Test",
"UnitTests"
"UnitTests",
"Verify"
]
},
"Verbosity": {
Expand Down
18 changes: 13 additions & 5 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
<Project>

<!-- Enable new dotnet test experience for .NET 10 + Microsoft Testing Platform -->
<!-- Applied to all projects; only test projects (with MTP packages) will use it -->
<PropertyGroup>
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
</PropertyGroup>
<!-- SDK enables TreatWarningsAsErrors in CI/Release. The list below carves out specific
rule IDs that surface heavily on the existing codebase and are scheduled for
follow-up cleanup PRs rather than the SDK-adoption PR. Each ID has a target PR. -->
<PropertyGroup>
<WarningsNotAsErrors>
$(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
</WarningsNotAsErrors>
</PropertyGroup>

</Project>
14 changes: 11 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<!-- Apply central versions to transitive deps too, so the GHSA pins below propagate
into Pipeline (which still runs on Microsoft.NET.Sdk for NUKE compatibility). -->
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>

<ItemGroup>
Expand Down Expand Up @@ -36,9 +39,7 @@
<PackageVersion Include="Microsoft.Extensions.Telemetry.Abstractions" Version="$(MicrosoftExtensionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="$(MicrosoftExtensionsVersion)" />

<!-- Test stack -->
<PackageVersion Include="AwesomeAssertions" Version="$(AwesomeAssertionsVersion)" />
<PackageVersion Include="AwesomeAssertions.Analyzers" Version="$(AwesomeAssertionsAnalyzersVersion)" />
<!-- Test stack (AwesomeAssertions + AwesomeAssertions.Analyzers come from ANcpLua.NET.Sdk.Test). -->
<PackageVersion Include="MartinCostello.Logging.XUnit.v3" Version="$(MartinCostelloLoggingXUnitV3Version)" />
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="$(MicrosoftTestingExtensionsCodeCoverageVersion)" />
<PackageVersion Include="Microsoft.Testing.Extensions.TrxReport" Version="$(MicrosoftTestingExtensionsTrxReportVersion)" />
Expand Down Expand Up @@ -73,6 +74,13 @@
<PackageVersion Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
<PackageVersion Include="Scalar.AspNetCore" Version="$(ScalarAspNetCoreVersion)" />
<PackageVersion Include="SWEN3.Paperless.RabbitMq" Version="$(SwenPaperlessRabbitMqVersion)" />

<!-- Transitive security overrides: close GHSA-g4vj-cjjj-v7hg (NuGet.Packaging 6.12.1
pulled by Nuke.Common) and GHSA-37gx-xxp4-5rgx / GHSA-w3x6-4m5h-cxqf
(System.Security.Cryptography.Xml 9.0.0). Effective because the SDK turns on
CentralPackageTransitivePinningEnabled. -->
<PackageVersion Include="NuGet.Packaging" Version="7.3.1" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.7" />
Comment on lines +82 to +83
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 HIGH RISK

Update the package versions to match the stable releases mentioned in the comment to ensure a successful build. The versions 7.3.1 and 10.0.7 do not exist for these packages.

Suggested change
<PackageVersion Include="NuGet.Packaging" Version="7.3.1" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.7" />
<PackageVersion Include="NuGet.Packaging" Version="6.12.1" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="9.0.1" />

</ItemGroup>

</Project>
6 changes: 1 addition & 5 deletions Paperless.Contracts/Paperless.Contracts.csproj
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="ANcpLua.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Paperless.Contracts</RootNamespace>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,11 +312,11 @@ 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 Task.Delay(10);
await Task.Delay(10, TestContext.Current.CancellationToken);

Document middle = new DocumentBuilder().WithFileName($"{testPrefix}-mid.pdf").Build();
await _repository.AddAsync(middle, TestContext.Current.CancellationToken);
await Task.Delay(10);
await Task.Delay(10, TestContext.Current.CancellationToken);

Document newest = new DocumentBuilder().WithFileName($"{testPrefix}-new.pdf").Build();
await _repository.AddAsync(newest, TestContext.Current.CancellationToken);
Expand Down Expand Up @@ -345,7 +345,7 @@ public async Task GetDocumentsPagedAsync_RespectsPageSize()
await _repository.AddAsync(
new DocumentBuilder().WithFileName($"{testPrefix}-{i}.pdf").Build(),
TestContext.Current.CancellationToken);
await Task.Delay(5); // Ensure distinct GUIDv7s
await Task.Delay(5, TestContext.Current.CancellationToken); // Ensure distinct GUIDv7s
}

// Act
Expand All @@ -369,7 +369,7 @@ public async Task GetDocumentsPagedAsync_WithCursor_ReturnsNextPage()
Document doc = new DocumentBuilder().WithFileName($"{testPrefix}-{i}.pdf").Build();
Document added = await _repository.AddAsync(doc, TestContext.Current.CancellationToken);
addedDocs.Add(added);
await Task.Delay(5);
await Task.Delay(5, TestContext.Current.CancellationToken);
}

// Act - Get first page
Expand Down Expand Up @@ -400,7 +400,7 @@ public async Task GetDocumentsPagedAsync_LastPage_HasMoreIsFalse()
await _repository.AddAsync(
new DocumentBuilder().WithFileName($"{testPrefix}-{i}.pdf").Build(),
TestContext.Current.CancellationToken);
await Task.Delay(5);
await Task.Delay(5, TestContext.Current.CancellationToken);
}

// Act - Request more than available
Expand Down
26 changes: 9 additions & 17 deletions PaperlessREST.Tests/PaperlessREST.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="ANcpLua.NET.Sdk.Web">
<!--
IMPORTANT: Web SDK is required for WebApplicationFactory + MTP self-hosted tests.
Do NOT switch back to Microsoft.NET.Sdk - integration tests will break.
See: https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests
IsTestProject=true opts this Web-SDK project into ANcpLua.NET.Sdk's Tests.targets
(MTP detection, AwesomeAssertions implicit, GitHub Actions test logger).
-->

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<!-- ANcpLua.NET.Sdk's Tests.targets sets these at BeforeBuild; declare here too so the
property graph is correct at restore/eval time (Web SDK ships OutputType=Exe but is
conservative about MTP detection). -->
<UseMicrosoftTestingPlatform>true</UseMicrosoftTestingPlatform>
<RootNamespace>PaperlessREST.Tests</RootNamespace>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<!-- Enable new dotnet test experience for .NET 10 + MTP v2 -->
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AwesomeAssertions"/>
<PackageReference Include="AwesomeAssertions.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- AwesomeAssertions + AwesomeAssertions.Analyzers are injected by ANcpLua.NET.Sdk.Test (IsTestProject=true). -->
<PackageReference Include="Testably.Abstractions"/>
<PackageReference Include="Testably.Abstractions.FileSystem.Interface"/>
<PackageReference Include="Testably.Abstractions.Testing"/>
Expand Down
6 changes: 3 additions & 3 deletions PaperlessREST.Tests/Unit/DocumentServiceErrorMappingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public async Task UploadDocumentAsync_StorageTimeout_ReturnsStorageTimeoutError(
DocumentService sut = CreateSut();

// Act
ErrorOr<Document> result = await sut.UploadDocumentAsync(request);
ErrorOr<Document> result = await sut.UploadDocumentAsync(request, TestContext.Current.CancellationToken);

// Assert
result.IsError.Should().BeTrue();
Expand All @@ -57,7 +57,7 @@ public async Task UploadDocumentAsync_Storage500_ReturnsStorageServerError()
DocumentService sut = CreateSut();

// Act
ErrorOr<Document> result = await sut.UploadDocumentAsync(request);
ErrorOr<Document> result = await sut.UploadDocumentAsync(request, TestContext.Current.CancellationToken);

// Assert
result.IsError.Should().BeTrue();
Expand All @@ -80,7 +80,7 @@ public async Task UploadDocumentAsync_StorageConnectionRefused_ReturnsStorageCon
DocumentService sut = CreateSut();

// Act
ErrorOr<Document> result = await sut.UploadDocumentAsync(request);
ErrorOr<Document> result = await sut.UploadDocumentAsync(request, TestContext.Current.CancellationToken);

// Assert
result.IsError.Should().BeTrue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public sealed class ReportProcessor(
ILogger<ReportProcessor> logger) : IReportProcessor
{
private const string SchemaFileName = "accessReport.xsd";
private static readonly XmlSerializer Serializer = new(typeof(AccessReportDto));
private static readonly XmlSerializer s_serializer = new(typeof(AccessReportDto));

private XmlSchemaSet Schemas => field ??= LoadSchemas();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ LOW RISK

Suggestion: Lazy initialization with field ??= is not thread-safe. Concurrent requests may cause LoadSchemas() to run multiple times. Consider using LazyInitializer.EnsureInitialized or a Lazy<XmlSchemaSet> field to ensure the schema set is built only once in a thread-safe manner.


Expand Down Expand Up @@ -89,7 +89,7 @@ private XmlSchemaSet LoadSchemas()
};

using XmlReader reader = XmlReader.Create(stream, settings);
AccessReportDto dto = (AccessReportDto)Serializer.Deserialize(reader)!;
AccessReportDto dto = (AccessReportDto)s_serializer.Deserialize(reader)!;

if (validationErrors.Count > 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public sealed class Document
/// Creates a new <see cref="Document" /> instance from an uploaded file.
/// </summary>
/// <param name="fileName">The original filename of the uploaded PDF.</param>
/// <param name="timeProvider">Provides the UTC timestamp recorded on <see cref="CreatedAt" />; inject for testability.</param>
/// <returns>A new <see cref="Document" /> in <see cref="DocumentStatus.Pending" /> status.</returns>
/// <remarks>
/// This factory method initializes a document with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,6 @@ await Task.WhenAll(
"Document.StorageConnectionFailed",
$"Cannot connect to storage service for {storagePath}"),

_ => null!
_ => null
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ private IServiceCollection AddInfrastructure(IConfiguration config)

private IServiceCollection AddPostgres(IConfiguration config)
{
NpgsqlDataSource dataSource = new NpgsqlDataSourceBuilder(config.GetConnectionString("PaperlessDb")!)
NpgsqlDataSource dataSource = new NpgsqlDataSourceBuilder(config.GetConnectionString("PaperlessDb"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM RISK

Add a guard clause to handle missing connection strings explicitly, which improves error clarity and satisfies the compiler when NRT is enabled.

Suggested change
NpgsqlDataSource dataSource = new NpgsqlDataSourceBuilder(config.GetConnectionString("PaperlessDb"))
string connectionString = config.GetConnectionString("PaperlessDb") ?? throw new InvalidOperationException("Connection string 'PaperlessDb' not found.");
NpgsqlDataSource dataSource = new NpgsqlDataSourceBuilder(connectionString)

.MapEnum<DocumentStatus>("document_status")
.Build();

Expand Down
9 changes: 3 additions & 6 deletions PaperlessREST/Host/Extensions/TypedErrorOrAsyncExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ namespace PaperlessREST.Host.Extensions;
/// </summary>
public static class TypedErrorOrAsyncExtensions
{
private static readonly NotFound NotFound = TypedResults.NotFound();
private static readonly NoContent NoContent = TypedResults.NoContent();

private static ValidationProblem CreateValidationProblem(IReadOnlyList<Error> errors) =>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ LOW RISK

Nitpick: Re-introduce the cached static instances for NotFound and NoContent results to maintain performance and reduce allocations in high-traffic scenarios.

TypedResults.ValidationProblem(
errors.Where(e => e.Type == ErrorType.Validation)
Expand Down Expand Up @@ -79,7 +76,7 @@ public async Task<Results<Ok<TResult>, NotFound>> ToOkOr404<TResult>(
}

return result.FirstError.Type == ErrorType.NotFound
? NotFound
? TypedResults.NotFound()
: throw ContractViolationException.ForNotFoundOnly(result.FirstError, result.Errors, callerName);
}

Expand Down Expand Up @@ -126,11 +123,11 @@ public async Task<Results<NoContent, NotFound>> ToNoContentOr404([CallerMemberNa

if (!result.IsError)
{
return NoContent;
return TypedResults.NoContent();
}

return result.FirstError.Type == ErrorType.NotFound
? NotFound
? TypedResults.NotFound()
: throw ContractViolationException.ForNotFoundOnly(result.FirstError, result.Errors, callerName);
}
}
Expand Down
13 changes: 2 additions & 11 deletions PaperlessREST/PaperlessREST.csproj
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="ANcpLua.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<LangVersion>preview</LangVersion>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn> <!-- Suppress missing XML comment warnings for now -->
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
Expand Down
19 changes: 5 additions & 14 deletions PaperlessServices.Tests/PaperlessServices.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="ANcpLua.NET.Sdk.Test">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- ANcpLua.NET.Sdk.Test sets these at target-eval (BeforeBuild), but NUKE / dotnet test
queries the static property graph at restore time and needs OutputType=Exe up front. -->
<UseMicrosoftTestingPlatform>true</UseMicrosoftTestingPlatform>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<!-- Enable new dotnet test experience for .NET 10 + MTP v2 -->
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AwesomeAssertions"/>
<PackageReference Include="AwesomeAssertions.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- AwesomeAssertions + AwesomeAssertions.Analyzers are injected by ANcpLua.NET.Sdk.Test. -->
<PackageReference Include="CreatePdf.NET"/>
<PackageReference Include="DotNetEnv"/>
<PackageReference Include="MartinCostello.Logging.XUnit.v3"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ public class SearchIndexService(
ILogger<SearchIndexService> logger)
: ISearchIndexService
{
private static readonly SemaphoreSlim SInitLock = new(1, 1);
private static readonly ConcurrentDictionary<string, bool> SInitializedIndices = new();
private static readonly SemaphoreSlim s_initLock = new(1, 1);
private static readonly ConcurrentDictionary<string, bool> s_initializedIndices = new();

/// <summary>
/// Indexes a document in Elasticsearch after OCR processing completes.
Expand Down Expand Up @@ -77,24 +77,24 @@ private async Task InitializeAsync(CancellationToken cancellationToken = default
string indexName = options.Value.DefaultIndex;

// Fast path: index already initialized
if (SInitializedIndices.ContainsKey(indexName))
if (s_initializedIndices.ContainsKey(indexName))
{
return;
}

await SInitLock.WaitAsync(cancellationToken);
await s_initLock.WaitAsync(cancellationToken);
try
{
// Double-check after acquiring lock
if (SInitializedIndices.ContainsKey(indexName))
if (s_initializedIndices.ContainsKey(indexName))
{
return;
}

ExistsResponse existsResponse = await elastic.Indices.ExistsAsync(indexName, cancellationToken);
if (existsResponse.Exists)
{
SInitializedIndices.TryAdd(indexName, true);
s_initializedIndices.TryAdd(indexName, true);
return;
}

Expand All @@ -113,11 +113,11 @@ private async Task InitializeAsync(CancellationToken cancellationToken = default
logger.LogInformation("Created Elasticsearch index: {IndexName}", indexName);
}

SInitializedIndices.TryAdd(indexName, true);
s_initializedIndices.TryAdd(indexName, true);
}
finally
{
SInitLock.Release();
s_initLock.Release();
}
}
}
18 changes: 12 additions & 6 deletions PaperlessServices/PaperlessServices.csproj
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<Project Sdk="ANcpLua.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OutputType>Exe</OutputType>
<UserSecretsId>paperless-services</UserSecretsId>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="PaperlessServices.Tests"/>
</ItemGroup>

<!-- Microsoft.NET.Sdk.Worker injected these as implicit usings; ANcpLua.NET.Sdk inherits
Microsoft.NET.Sdk, so worker-host namespaces need to be opted in here. -->
<ItemGroup>
<Using Include="Microsoft.Extensions.Configuration"/>
<Using Include="Microsoft.Extensions.DependencyInjection"/>
<Using Include="Microsoft.Extensions.Hosting"/>
<Using Include="Microsoft.Extensions.Logging"/>
</ItemGroup>

<ItemGroup>
<PackageReference Include="CreatePdf.NET"/>
<PackageReference Include="DotNetEnv"/>
Expand Down
Loading