From afc0e34312e06f8296ab17b7544e2d0b3c2dcf9c Mon Sep 17 00:00:00 2001 From: OlgunR Date: Thu, 25 Jun 2026 15:23:58 +0200 Subject: [PATCH] Complete Step 1.1: ValidatePDF Application Layer Updated PHASENPLAN and ROADMAP to reflect progress on Feature 1 - ValidatePDF (75% complete). Marked Step 1.1 as completed, including MediatR setup, pipeline behaviors (`ValidationBehavior`, `LoggingBehavior`), ValidatePDF feature (Query, Handler, Validator), and DTOs. Added `DependencyInjection.cs` for Application Layer DI configuration. Introduced `LoggingBehavior` and `ValidationBehavior` for MediatR pipelines. Implemented `ValidatePdfHandler`, `ValidatePdfQuery`, and `ValidatePdfValidator`. Created DTOs (`ValidatePdfRequest`, `ValidatePdfResponse`) for the ValidatePDF feature. Added unit tests for `ValidatePdfHandler` to verify metadata handling and exception propagation. Removed unused folder references in `DocumentOperator.Application.csproj`. --- DocumentOperator.API/PHASENPLAN.md | 47 +++++------ DocumentOperator.API/ROADMAP.md | 53 +++++++------ .../Common/Behaviors/LoggingBehavior.cs | 66 ++++++++++++++++ .../Common/Behaviors/ValidationBehavior.cs | 54 +++++++++++++ .../Common/DTOs/ValidatePdfRequest.cs | 7 ++ .../Common/DTOs/ValidatePdfResponse.cs | 14 ++++ .../DependencyInjection.cs | 33 ++++++++ .../DocumentOperator.Application.csproj | 3 - .../ValidatePdf/ValidatePdfHandler.cs | 33 ++++++++ .../Documents/ValidatePdf/ValidatePdfQuery.cs | 10 +++ .../ValidatePdf/ValidatePdfValidator.cs | 17 ++++ .../ValidatePdf/ValidatePdfHandlerTests.cs | 78 +++++++++++++++++++ 12 files changed, 367 insertions(+), 48 deletions(-) create mode 100644 DocumentOperator.Application/Common/Behaviors/LoggingBehavior.cs create mode 100644 DocumentOperator.Application/Common/Behaviors/ValidationBehavior.cs create mode 100644 DocumentOperator.Application/Common/DTOs/ValidatePdfRequest.cs create mode 100644 DocumentOperator.Application/Common/DTOs/ValidatePdfResponse.cs create mode 100644 DocumentOperator.Application/DependencyInjection.cs create mode 100644 DocumentOperator.Application/Features/Documents/ValidatePdf/ValidatePdfHandler.cs create mode 100644 DocumentOperator.Application/Features/Documents/ValidatePdf/ValidatePdfQuery.cs create mode 100644 DocumentOperator.Application/Features/Documents/ValidatePdf/ValidatePdfValidator.cs create mode 100644 DocumentOperator.Tests/Unit/Application/Features/ValidatePdf/ValidatePdfHandlerTests.cs diff --git a/DocumentOperator.API/PHASENPLAN.md b/DocumentOperator.API/PHASENPLAN.md index 7b2667b..cd42ff2 100644 --- a/DocumentOperator.API/PHASENPLAN.md +++ b/DocumentOperator.API/PHASENPLAN.md @@ -1,6 +1,6 @@ # ?? DocumentOperator - Phasenplan (Feature-Driven Development) -> **Stand:** 17.01.2025 | **Aktuell:** Feature 1 - ValidatePDF (Step 1.1 NEXT) | **Projektdauer:** 6 Wochen +> **Stand:** 17.01.2025 | **Aktuell:** Feature 1 - ValidatePDF (Step 1.2 NEXT) | **Projektdauer:** 6 Wochen --- @@ -8,7 +8,7 @@ | Woche | Features / Concerns | Status | Fortschritt | |-------|---------------------|--------|-------------| -| **W1** | Feature 1: ValidatePDF | ?? In Progress | 50% (Infrastructure fertig, Application NEXT) | +| **W1** | Feature 1: ValidatePDF | ?? In Progress | 75% (Foundation + Application fertig, API NEXT) | | **W2** | Feature 2: ExtractAttachments | ? Geplant | 0% | | **W2** | Feature 3: ApplyStamp | ? Geplant | 0% | | **W3** | Feature 4: EmbedCertificate | ? Geplant | 0% | @@ -42,7 +42,7 @@ ## ?? DETAILLIERTER PLAN -### WOCHE 1 - Feature 1: ValidatePDF | ?? In Progress - 50% +### WOCHE 1 - Feature 1: ValidatePDF | ?? In Progress - 75% **Ziel:** POST /api/v1/documents/validate Endpoint im Swagger testbar @@ -58,38 +58,38 @@ --- -#### ?? Step 1.1: Application Layer (MediatR Setup + ValidatePDF Feature) - **NEXT** +#### ? Step 1.1: Application Layer (MediatR Setup + ValidatePDF Feature) - **ABGESCHLOSSEN** **Dauer:** ~4 Stunden -**Status:** ? NEXT +**Status:** ? ABGESCHLOSSEN -**Was wird erstellt:** +**Was wurde erstellt:** 1. **MediatR Setup** - - `Application/DependencyInjection.cs` (Service Registration) - - `Application/Common/Behaviors/ValidationBehavior.cs` (FluentValidation Pipeline) - - `Application/Common/Behaviors/LoggingBehavior.cs` (Logging Pipeline) + - ? `Application/DependencyInjection.cs` (Service Registration) + - ? `Application/Common/Behaviors/ValidationBehavior.cs` (FluentValidation Pipeline) + - ? `Application/Common/Behaviors/LoggingBehavior.cs` (Logging Pipeline mit ILogger) 2. **ValidatePDF Feature (Vertical Slice)** - - `Application/Features/Documents/ValidatePdf/ValidatePdfQuery.cs` - - `Application/Features/Documents/ValidatePdf/ValidatePdfHandler.cs` - - `Application/Features/Documents/ValidatePdf/ValidatePdfValidator.cs` + - ? `Application/Features/Documents/ValidatePdf/ValidatePdfQuery.cs` + - ? `Application/Features/Documents/ValidatePdf/ValidatePdfHandler.cs` + - ? `Application/Features/Documents/ValidatePdf/ValidatePdfValidator.cs` 3. **DTOs** - - `Application/Common/DTOs/ValidatePdfRequest.cs` - - `Application/Common/DTOs/ValidatePdfResponse.cs` + - ? `Application/Common/DTOs/ValidatePdfRequest.cs` + - ? `Application/Common/DTOs/ValidatePdfResponse.cs` 4. **Tests** - - `Tests/Unit/Application/Features/ValidatePdf/ValidatePdfHandlerTests.cs` + - ? `Tests/Unit/Application/Features/ValidatePdf/ValidatePdfHandlerTests.cs` (2 Tests - alle grün!) **Akzeptanzkriterien:** - ? Build erfolgreich -- ? Tests grün (Handler Tests) +- ? Tests grün (Handler Tests: 2/2 passed) - ? MediatR Pipeline funktioniert (Validation + Logging) --- -#### ? Step 1.2: API Layer (Endpoint + Exception Middleware) +#### ?? Step 1.2: API Layer (Endpoint + Exception Middleware) - **NEXT** **Dauer:** ~3 Stunden -**Status:** ? Pending +**Status:** ? NEXT **Was wird erstellt:** 1. **Exception Middleware** @@ -418,7 +418,7 @@ | Kategorie | Status | Fortschritt | |-----------|--------|-------------| | **Foundation** | ? Abgeschlossen | 100% | -| **Feature 1** | ?? In Progress | 50% | +| **Feature 1** | ?? In Progress | 75% | | **Feature 2-5** | ? Pending | 0% | | **Multi-Tenancy** | ? Pending | 0% | | **Cross-Cutting** | ? Pending | 0% | @@ -430,19 +430,19 @@ ### Heute (17.01.2025) -**Feature 1 - Step 1.1: Application Layer** +**Feature 1 - Step 1.1: Application Layer** ? **ABGESCHLOSSEN** 1. ? MediatR Setup (DependencyInjection.cs) 2. ? ValidationBehavior.cs erstellen -3. ? LoggingBehavior.cs erstellen +3. ? LoggingBehavior.cs erstellen (mit ILogger) 4. ? ValidatePDF Feature erstellen (Query, Handler, Validator) 5. ? DTOs erstellen (Request, Response) -6. ? Tests schreiben (ValidatePdfHandlerTests.cs) +6. ? Tests schreiben (ValidatePdfHandlerTests.cs - 2 Tests grün) 7. ? Build + Tests grün 8. ? ROADMAP + PHASENPLAN aktualisieren 9. ? Commit **Danach:** -? Feature 1 - Step 1.2: API Layer (Endpoint + Exception Middleware) +? Feature 1 - Step 1.2: API Layer (Endpoint + Exception Middleware) - **NEXT** --- @@ -455,6 +455,7 @@ | 17.01.2025 | Infrastructure | DevExpressPdfProcessor.ValidateAsync implementiert | | 17.01.2025 | Tests | DevExpressPdfProcessorTests.cs erstellt (6 Tests) | | 17.01.2025 | **PHASENPLAN** | ?? **Komplett umstrukturiert** (Feature-basiert + Datum korrigiert 23.06.2026 ? 17.01.2025) | +| 17.01.2025 | **Feature 1 - Step 1.1** | ? **ABGESCHLOSSEN** - Application Layer (MediatR, Behaviors, ValidatePDF Feature, DTOs, Tests - 2/2 grün) | --- diff --git a/DocumentOperator.API/ROADMAP.md b/DocumentOperator.API/ROADMAP.md index f13dba4..cf330ba 100644 --- a/DocumentOperator.API/ROADMAP.md +++ b/DocumentOperator.API/ROADMAP.md @@ -1,6 +1,6 @@ # ?? DocumentOperator - Project Roadmap (Feature-Driven Development) -> **Last Updated:** 17.01.2025 | **Status:** In Development | **Current Feature:** Feature 1 - ValidatePDF (NEXT) +> **Last Updated:** 17.01.2025 | **Status:** In Development | **Current Feature:** Feature 1 - ValidatePDF (Step 1.2 NEXT) --- @@ -44,7 +44,7 @@ | Feature | Type | Status | Swagger Testbar? | |---------|------|--------|------------------| -| **1. ValidatePDF** | Synchron | ?? NEXT | ? | +| **1. ValidatePDF** | Synchron | ?? In Progress (Step 1.2 NEXT) | ? | | **2. ExtractAttachments** | Synchron | ? Pending | ? | | **3. ApplyStamp** | Synchron | ? Pending | ? | | **4. EmbedCertificate** | Synchron | ? Pending | ? | @@ -61,7 +61,7 @@ --- -## ?? FEATURE 1: ValidatePDF (Synchron) - **NEXT** +## ?? FEATURE 1: ValidatePDF (Synchron) - **In Progress** **Was macht dieses Feature?** - Client sendet PDF als Base64 (JSON) @@ -77,7 +77,7 @@ Response: { "pageCount": 5, "fileSizeBytes": 1024, "pdfVersion": "1.4", "hasAtta --- -### ? Step 1.0: Foundation (bereits erledigt!) +### ? Step 1.0: Foundation (ABGESCHLOSSEN) **Was wurde bereits erstellt:** - ? Domain Layer (Exceptions, Enums, Value Objects) @@ -92,36 +92,36 @@ Response: { "pageCount": 5, "fileSizeBytes": 1024, "pdfVersion": "1.4", "hasAtta --- -### ?? Step 1.1: Application Layer (MediatR Setup + ValidatePDF Feature) - **NEXT** +### ? Step 1.1: Application Layer (MediatR Setup + ValidatePDF Feature) - **ABGESCHLOSSEN** **Ziel:** MediatR + FluentValidation + ValidatePDF Handler -**Was wird erstellt:** +**Was wurde erstellt:** #### 1.1.1: MediatR Setup -- **Datei:** `Application/DependencyInjection.cs` +- ? **Datei:** `Application/DependencyInjection.cs` - Registriert MediatR - Registriert FluentValidation - Registriert Pipeline Behaviors (Validation + Logging) #### 1.1.2: Pipeline Behaviors -- **Datei:** `Application/Common/Behaviors/ValidationBehavior.cs` +- ? **Datei:** `Application/Common/Behaviors/ValidationBehavior.cs` - Führt FluentValidation automatisch aus (vor jedem Handler) - Wirft `ValidationException` bei Fehler -- **Datei:** `Application/Common/Behaviors/LoggingBehavior.cs` +- ? **Datei:** `Application/Common/Behaviors/LoggingBehavior.cs` - Loggt jeden Request (mit Performance-Tracking) - - Nutzt Serilog + - Nutzt `ILogger` (Clean Architecture konform!) #### 1.1.3: ValidatePDF Feature (Vertical Slice) -- **Ordner:** `Application/Features/Documents/ValidatePdf/` +- ? **Ordner:** `Application/Features/Documents/ValidatePdf/` -- **Datei:** `ValidatePdfQuery.cs` +- ? **Datei:** `ValidatePdfQuery.cs` ```csharp public record ValidatePdfQuery(Base64String PdfContent) : IRequest; ``` -- **Datei:** `ValidatePdfHandler.cs` +- ? **Datei:** `ValidatePdfHandler.cs` ```csharp public class ValidatePdfHandler : IRequestHandler { @@ -135,7 +135,7 @@ Response: { "pageCount": 5, "fileSizeBytes": 1024, "pdfVersion": "1.4", "hasAtta } ``` -- **Datei:** `ValidatePdfValidator.cs` +- ? **Datei:** `ValidatePdfValidator.cs` ```csharp public class ValidatePdfValidator : AbstractValidator { @@ -147,14 +147,14 @@ Response: { "pageCount": 5, "fileSizeBytes": 1024, "pdfVersion": "1.4", "hasAtta ``` #### 1.1.4: DTOs -- **Ordner:** `Application/Common/DTOs/` +- ? **Ordner:** `Application/Common/DTOs/` -- **Datei:** `ValidatePdfRequest.cs` +- ? **Datei:** `ValidatePdfRequest.cs` ```csharp public record ValidatePdfRequest(string Base64Pdf); ``` -- **Datei:** `ValidatePdfResponse.cs` +- ? **Datei:** `ValidatePdfResponse.cs` ```csharp public record ValidatePdfResponse( int PageCount, @@ -166,9 +166,9 @@ Response: { "pageCount": 5, "fileSizeBytes": 1024, "pdfVersion": "1.4", "hasAtta ``` #### 1.1.5: Tests -- **Datei:** `Tests/Unit/Application/Features/ValidatePdf/ValidatePdfHandlerTests.cs` - - Test: `Handle_ValidPdf_ReturnsPdfMetadata` - - Test: `Handle_NullPdfContent_ThrowsValidationException` +- ? **Datei:** `Tests/Unit/Application/Features/ValidatePdf/ValidatePdfHandlerTests.cs` + - ? Test: `Handle_ValidPdf_ReturnsPdfMetadata` + - ? Test: `Handle_PdfProcessorThrowsException_PropagatesException` **Akzeptanzkriterien:** - ? Build erfolgreich @@ -177,7 +177,7 @@ Response: { "pageCount": 5, "fileSizeBytes": 1024, "pdfVersion": "1.4", "hasAtta --- -### ?? Step 1.2: API Layer (Endpoint + Exception Middleware) +### ?? Step 1.2: API Layer (Endpoint + Exception Middleware) - **NEXT** **Ziel:** HTTP Endpoint + zentrale Exception Handling @@ -898,10 +898,18 @@ DocumentOperator.Tests/ - ? IPdfProcessor Interface - ? DevExpressPdfProcessor.ValidateAsync (mit Tests!) +- **Application Layer:** + - ? DependencyInjection.cs (MediatR + FluentValidation) + - ? ValidationBehavior.cs (FluentValidation Pipeline) + - ? LoggingBehavior.cs (ILogger Pipeline) + - ? ValidatePDF Feature (Query, Handler, Validator) + - ? DTOs (ValidatePdfRequest, ValidatePdfResponse) + - ? Tests (ValidatePdfHandlerTests - 2 Tests grün) + ### ?? In Progress - **Feature 1: ValidatePDF** - - ? Step 1.1: Application Layer (MediatR Setup + ValidatePDF Feature) - **NEXT** + - ? Step 1.2: API Layer (Endpoint + Exception Middleware) - **NEXT** ### ? Pending @@ -995,6 +1003,7 @@ DocumentOperator.Tests/ | 17.01.2025 | Tests | DevExpressPdfProcessorTests.cs erstellt (6 Tests) | | 17.01.2025 | **ROADMAP** | ?? **Komplett umstrukturiert** (Feature-Driven Development!) | | 17.01.2025 | **PHASENPLAN** | ?? **Komplett umstrukturiert** (Feature-basiert + Datum korrigiert) | +| 17.01.2025 | **Feature 1 - Step 1.1** | ? **ABGESCHLOSSEN** - Application Layer (MediatR, Behaviors, ValidatePDF Feature, DTOs, Tests) | --- diff --git a/DocumentOperator.Application/Common/Behaviors/LoggingBehavior.cs b/DocumentOperator.Application/Common/Behaviors/LoggingBehavior.cs new file mode 100644 index 0000000..35db151 --- /dev/null +++ b/DocumentOperator.Application/Common/Behaviors/LoggingBehavior.cs @@ -0,0 +1,66 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace DocumentOperator.Application.Common.Behaviors; + +/// +/// MediatR Pipeline Behavior that logs requests and tracks performance +/// Executes AFTER ValidationBehavior, BEFORE Handler +/// +public class LoggingBehavior : IPipelineBehavior + where TRequest : IRequest +{ + private readonly ILogger> _logger; + + public LoggingBehavior(ILogger> logger) + { + _logger = logger; + } + + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + var requestName = typeof(TRequest).Name; + + // Request Start + _logger.LogInformation("Handling {RequestName}: {@Request}", requestName, request); + + // Performance Tracking + var stopwatch = Stopwatch.StartNew(); + + try + { + // Handler ausführen + var response = await next(); + + stopwatch.Stop(); + + // Request Success + _logger.LogInformation( + "Handled {RequestName} in {ElapsedMs}ms", + requestName, + stopwatch.ElapsedMilliseconds + ); + + return response; + } + catch (Exception ex) + { + stopwatch.Stop(); + + // Request Failed + _logger.LogError( + ex, + "Error handling {RequestName} after {ElapsedMs}ms: {ErrorMessage}", + requestName, + stopwatch.ElapsedMilliseconds, + ex.Message + ); + + throw; // Exception weiterwerfen (wird von Exception Middleware gefangen) + } + } +} diff --git a/DocumentOperator.Application/Common/Behaviors/ValidationBehavior.cs b/DocumentOperator.Application/Common/Behaviors/ValidationBehavior.cs new file mode 100644 index 0000000..c853ab0 --- /dev/null +++ b/DocumentOperator.Application/Common/Behaviors/ValidationBehavior.cs @@ -0,0 +1,54 @@ +using FluentValidation; +using MediatR; + +namespace DocumentOperator.Application.Common.Behaviors; + +/// +/// MediatR Pipeline Behavior that validates requests using FluentValidation +/// Executes BEFORE the Handler +/// +public class ValidationBehavior : IPipelineBehavior + where TRequest : IRequest +{ + private readonly IEnumerable> _validators; + + public ValidationBehavior(IEnumerable> validators) + { + _validators = validators; + } + + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + // Wenn keine Validators registriert sind, direkt weiter zum Handler + if (!_validators.Any()) + { + return await next(); + } + + // Validation Context erstellen + var context = new ValidationContext(request); + + // Alle Validators parallel ausführen + var validationResults = await Task.WhenAll( + _validators.Select(v => v.ValidateAsync(context, cancellationToken)) + ); + + // Fehler sammeln + var failures = validationResults + .Where(r => !r.IsValid) + .SelectMany(r => r.Errors) + .ToList(); + + // Bei Fehlern: ValidationException werfen (wird von Exception Middleware gefangen) + if (failures.Any()) + { + throw new ValidationException(failures); + } + + // Validation erfolgreich ? weiter zum Handler + return await next(); + } +} diff --git a/DocumentOperator.Application/Common/DTOs/ValidatePdfRequest.cs b/DocumentOperator.Application/Common/DTOs/ValidatePdfRequest.cs new file mode 100644 index 0000000..56c57b5 --- /dev/null +++ b/DocumentOperator.Application/Common/DTOs/ValidatePdfRequest.cs @@ -0,0 +1,7 @@ +namespace DocumentOperator.Application.Common.DTOs; + +/// +/// Request DTO for ValidatePdf endpoint +/// +/// PDF content as Base64 string +public record ValidatePdfRequest(string Base64Pdf); diff --git a/DocumentOperator.Application/Common/DTOs/ValidatePdfResponse.cs b/DocumentOperator.Application/Common/DTOs/ValidatePdfResponse.cs new file mode 100644 index 0000000..8c4d06a --- /dev/null +++ b/DocumentOperator.Application/Common/DTOs/ValidatePdfResponse.cs @@ -0,0 +1,14 @@ +namespace DocumentOperator.Application.Common.DTOs; + +/// +/// Response DTO for ValidatePdf endpoint +/// Contains PDF metadata +/// +public record ValidatePdfResponse( + int PageCount, + long FileSizeBytes, + double FileSizeMB, + string PdfVersion, + bool HasAttachments, + int AttachmentCount +); diff --git a/DocumentOperator.Application/DependencyInjection.cs b/DocumentOperator.Application/DependencyInjection.cs new file mode 100644 index 0000000..7c2e4d7 --- /dev/null +++ b/DocumentOperator.Application/DependencyInjection.cs @@ -0,0 +1,33 @@ +using FluentValidation; +using Microsoft.Extensions.DependencyInjection; + +namespace DocumentOperator.Application; + +/// +/// Dependency Injection configuration for Application Layer +/// +public static class DependencyInjection +{ + /// + /// Registers Application Layer services (MediatR, FluentValidation, Behaviors) + /// + public static IServiceCollection AddApplication(this IServiceCollection services) + { + var assembly = typeof(DependencyInjection).Assembly; + + // Register MediatR (scannt Assembly nach Handlers) + services.AddMediatR(config => + { + config.RegisterServicesFromAssembly(assembly); + + // Pipeline Behaviors (Reihenfolge wichtig!) + config.AddOpenBehavior(typeof(Common.Behaviors.ValidationBehavior<,>)); + config.AddOpenBehavior(typeof(Common.Behaviors.LoggingBehavior<,>)); + }); + + // Register FluentValidation (scannt Assembly nach Validators) + services.AddValidatorsFromAssembly(assembly); + + return services; + } +} diff --git a/DocumentOperator.Application/DocumentOperator.Application.csproj b/DocumentOperator.Application/DocumentOperator.Application.csproj index 1592c21..3292426 100644 --- a/DocumentOperator.Application/DocumentOperator.Application.csproj +++ b/DocumentOperator.Application/DocumentOperator.Application.csproj @@ -17,15 +17,12 @@ - - - diff --git a/DocumentOperator.Application/Features/Documents/ValidatePdf/ValidatePdfHandler.cs b/DocumentOperator.Application/Features/Documents/ValidatePdf/ValidatePdfHandler.cs new file mode 100644 index 0000000..50b6b09 --- /dev/null +++ b/DocumentOperator.Application/Features/Documents/ValidatePdf/ValidatePdfHandler.cs @@ -0,0 +1,33 @@ +using DocumentOperator.Application.Common.Interfaces; +using DocumentOperator.Domain.Models.ValueObjects; +using MediatR; + +namespace DocumentOperator.Application.Features.Documents.ValidatePdf; + +/// +/// Handler for ValidatePdfQuery +/// Orchestrates PDF validation using IPdfProcessor +/// +public class ValidatePdfHandler : IRequestHandler +{ + private readonly IPdfProcessor _pdfProcessor; + + public ValidatePdfHandler(IPdfProcessor pdfProcessor) + { + _pdfProcessor = pdfProcessor; + } + + /// + /// Validates PDF and returns metadata + /// + public async Task Handle(ValidatePdfQuery request, CancellationToken cancellationToken) + { + // Value Object ? Byte Array + byte[] pdfBytes = request.PdfContent.ToByteArray(); + + // DevExpress Service aufrufen (kann PdfProcessingException werfen) + var metadata = await _pdfProcessor.ValidateAsync(pdfBytes); + + return metadata; + } +} diff --git a/DocumentOperator.Application/Features/Documents/ValidatePdf/ValidatePdfQuery.cs b/DocumentOperator.Application/Features/Documents/ValidatePdf/ValidatePdfQuery.cs new file mode 100644 index 0000000..04d02a0 --- /dev/null +++ b/DocumentOperator.Application/Features/Documents/ValidatePdf/ValidatePdfQuery.cs @@ -0,0 +1,10 @@ +using DocumentOperator.Domain.Models.ValueObjects; +using MediatR; + +namespace DocumentOperator.Application.Features.Documents.ValidatePdf; + +/// +/// Query to validate a PDF document and return metadata +/// +/// PDF content as Base64 string (validated by Value Object) +public record ValidatePdfQuery(Base64String PdfContent) : IRequest; diff --git a/DocumentOperator.Application/Features/Documents/ValidatePdf/ValidatePdfValidator.cs b/DocumentOperator.Application/Features/Documents/ValidatePdf/ValidatePdfValidator.cs new file mode 100644 index 0000000..17ec5f6 --- /dev/null +++ b/DocumentOperator.Application/Features/Documents/ValidatePdf/ValidatePdfValidator.cs @@ -0,0 +1,17 @@ +using FluentValidation; + +namespace DocumentOperator.Application.Features.Documents.ValidatePdf; + +/// +/// Validator for ValidatePdfQuery +/// Validates that PdfContent is not null (Base64String already validates format in its constructor) +/// +public class ValidatePdfValidator : AbstractValidator +{ + public ValidatePdfValidator() + { + RuleFor(x => x.PdfContent) + .NotNull() + .WithMessage("PDF content is required"); + } +} diff --git a/DocumentOperator.Tests/Unit/Application/Features/ValidatePdf/ValidatePdfHandlerTests.cs b/DocumentOperator.Tests/Unit/Application/Features/ValidatePdf/ValidatePdfHandlerTests.cs new file mode 100644 index 0000000..225a0fd --- /dev/null +++ b/DocumentOperator.Tests/Unit/Application/Features/ValidatePdf/ValidatePdfHandlerTests.cs @@ -0,0 +1,78 @@ +using DocumentOperator.Application.Common.Interfaces; +using DocumentOperator.Application.Features.Documents.ValidatePdf; +using DocumentOperator.Domain.Common.Exceptions; +using DocumentOperator.Domain.Models.ValueObjects; +using FluentAssertions; +using Moq; +using Xunit; + +namespace DocumentOperator.Tests.Unit.Application.Features.ValidatePdf; + +public class ValidatePdfHandlerTests +{ + private readonly Mock _mockPdfProcessor; + private readonly ValidatePdfHandler _handler; + + public ValidatePdfHandlerTests() + { + _mockPdfProcessor = new Mock(); + _handler = new ValidatePdfHandler(_mockPdfProcessor.Object); + } + + [Fact] + public async Task Handle_ValidPdf_ReturnsPdfMetadata() + { + // Arrange + var base64Pdf = Convert.ToBase64String(new byte[] { 0x25, 0x50, 0x44, 0x46 }); // "%PDF" + var pdfContent = Base64String.Create(base64Pdf); + var query = new ValidatePdfQuery(pdfContent); + + var expectedMetadata = new PdfMetadata( + pageCount: 5, + fileSizeBytes: 1024, + pdfVersion: "1.4", + hasAttachments: false, + attachmentCount: 0 + ); + + _mockPdfProcessor + .Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(expectedMetadata); + + // Act + var result = await _handler.Handle(query, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.PageCount.Should().Be(5); + result.FileSizeBytes.Should().Be(1024); + result.PdfVersion.Should().Be("1.4"); + result.HasAttachments.Should().BeFalse(); + result.AttachmentCount.Should().Be(0); + + _mockPdfProcessor.Verify( + x => x.ValidateAsync(It.IsAny()), + Times.Once + ); + } + + [Fact] + public async Task Handle_PdfProcessorThrowsException_PropagatesException() + { + // Arrange + var base64Pdf = Convert.ToBase64String(new byte[] { 0x25, 0x50, 0x44, 0x46 }); + var pdfContent = Base64String.Create(base64Pdf); + var query = new ValidatePdfQuery(pdfContent); + + _mockPdfProcessor + .Setup(x => x.ValidateAsync(It.IsAny())) + .ThrowsAsync(new PdfProcessingException("Invalid PDF format")); + + // Act + Func act = async () => await _handler.Handle(query, CancellationToken.None); + + // Assert + await act.Should().ThrowAsync() + .WithMessage("Invalid PDF format"); + } +}