commit 0b9f4315a3dc38db36395cfb2fec87ae8cdd06d8
Author: Jens Kristoffersson <jens.kristoffersson.se@gmail.com>
AuthorDate: Wed Sep 2 09:07:11 2026 +0200
Commit: Jens Kristoffersson <jens.kristoffersson.se@gmail.com>
CommitDate: Wed Sep 2 09:07:11 2026 +0200
Lägg till debugOutputDir för manuellt testande
Test-only config: kopiera mergad PDF (försättsblad + remiss) till en
mapp innan utskrift/städning, så resultatet kan granskas utan att en
skrivare måste fungera. Tom i drift.
WorkflowTests: happy path (generate→merge→debugkopia→print via fake
IPrinter, 2 sidor) + felväg (utskriftsfel propageras, stage auditas).
17/17 gröna.
---
README.md | 1 +
config/appsettings.json | 3 +-
src/Remiss.Core/AppConfig.cs | 10 ++++
src/Remiss.Core/RemissWorkflow.cs | 15 +++++
tests/Remiss.Core.Tests/WorkflowTests.cs | 85 +++++++++++++++++++++++++++++
tools/SumatraPDF-settings.txt | 94 ++++++++++++++++++++++++++++++++
6 files changed, 207 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index a08ecb8..f3b94ff 100644
--- a/README.md
+++ b/README.md
@@ -95,6 +95,7 @@ tests/
| `errorLogPath` | Tom = `%LOCALAPPDATA%\Remiss\logs\error.log` |
| `printRetries` / `printRetryDelayMs` | Retry vid utskriftsfel |
| `deleteSourceAfterPrint` | `false` i Fas 1 (behåll test-PDF:en). `true` i Fas 2. |
+| `debugOutputDir` | **Endast test.** Satt = kopiera mergad PDF hit (patientdata, städas ej). Tom i drift. |
## Säkerhet
diff --git a/config/appsettings.json b/config/appsettings.json
index 0845cfd..6c3a5a3 100644
--- a/config/appsettings.json
+++ b/config/appsettings.json
@@ -7,5 +7,6 @@
"errorLogPath": "",
"printRetries": 3,
"printRetryDelayMs": 2000,
- "deleteSourceAfterPrint": false
+ "deleteSourceAfterPrint": false,
+ "debugOutputDir": ""
}
diff --git a/src/Remiss.Core/AppConfig.cs b/src/Remiss.Core/AppConfig.cs
index 5a9ff6d..1bf5776 100644
--- a/src/Remiss.Core/AppConfig.cs
+++ b/src/Remiss.Core/AppConfig.cs
@@ -19,6 +19,13 @@ public sealed class AppConfig
public int PrintRetryDelayMs { get; set; } = 2000;
public bool DeleteSourceAfterPrint { get; set; }
+ /// <summary>
+ /// ENDAST FÖR UTVECKLING/TEST. Om satt kopieras den mergade PDF:en (försättsblad
+ /// + remiss) hit innan utskrift och städning, så resultatet kan granskas.
+ /// Kopian innehåller patientdata och städas inte – lämna tom i drift.
+ /// </summary>
+ public string DebugOutputDir { get; set; } = "";
+
[JsonIgnore]
public string ConfigFilePath { get; private set; } = "";
@@ -58,6 +65,9 @@ public sealed class AppConfig
if (string.IsNullOrWhiteSpace(ErrorLogPath))
ErrorLogPath = Path.Combine(appRoot, "logs", "error.log");
+
+ if (!string.IsNullOrWhiteSpace(DebugOutputDir))
+ DebugOutputDir = ToAbsolute(DebugOutputDir, baseDir);
}
private static string ToAbsolute(string path, string baseDir) =>
diff --git a/src/Remiss.Core/RemissWorkflow.cs b/src/Remiss.Core/RemissWorkflow.cs
index fb3566b..240426e 100644
--- a/src/Remiss.Core/RemissWorkflow.cs
+++ b/src/Remiss.Core/RemissWorkflow.cs
@@ -37,6 +37,9 @@ public sealed class RemissWorkflow
stage = "merge";
PdfMerger.Merge(mergedPath, coverPath, sourcePdfPath);
+ stage = "debug-copy";
+ DumpForDebug(mergedPath);
+
stage = "print";
_printer.Print(mergedPath, _config.TargetPrinter);
@@ -54,4 +57,16 @@ public sealed class RemissWorkflow
throw;
}
}
+
+ // ENDAST TEST: kopiera ut mergad PDF (patientdata, städas inte) om debugOutputDir är satt.
+ private void DumpForDebug(string mergedPath)
+ {
+ if (string.IsNullOrWhiteSpace(_config.DebugOutputDir))
+ return;
+
+ Directory.CreateDirectory(_config.DebugOutputDir);
+ var target = Path.Combine(_config.DebugOutputDir, $"remiss-debug-{DateTime.Now:yyyyMMdd-HHmmss}.pdf");
+ File.Copy(mergedPath, target, overwrite: true);
+ _audit.Write("remiss.debugcopy", null, null, success: true, stage: "debug-copy", detail: "test-only");
+ }
}
diff --git a/tests/Remiss.Core.Tests/WorkflowTests.cs b/tests/Remiss.Core.Tests/WorkflowTests.cs
new file mode 100644
index 0000000..34b5cf8
--- /dev/null
+++ b/tests/Remiss.Core.Tests/WorkflowTests.cs
@@ -0,0 +1,85 @@
+using Remiss.Core;
+using Remiss.Core.Abstractions;
+using Remiss.Core.Audit;
+using Remiss.Core.Models;
+using Remiss.Core.Pdf;
+using Remiss.Core.Security;
+using Xunit;
+
+namespace Remiss.Core.Tests;
+
+public sealed class WorkflowTests : IDisposable
+{
+ private readonly string _dir = Path.Combine(Path.GetTempPath(), "remiss-wf-" + Guid.NewGuid().ToString("N"));
+
+ public WorkflowTests() => Directory.CreateDirectory(_dir);
+
+ public void Dispose()
+ {
+ try { Directory.Delete(_dir, recursive: true); } catch { /* ignore */ }
+ }
+
+ private sealed class FakePrinter : IPrinter
+ {
+ public string? PrintedFile { get; private set; }
+ public string? Printer { get; private set; }
+ public void Print(string pdfPath, string printerName)
+ {
+ Assert.True(File.Exists(pdfPath)); // mergad fil finns när utskrift anropas
+ PrintedFile = pdfPath;
+ Printer = printerName;
+ }
+ }
+
+ [Fact]
+ public void Run_generatesMergesPrintsAndWritesDebugCopy()
+ {
+ var source = Path.Combine(_dir, "remiss.pdf");
+ CoverPageGenerator.Generate(source, new CoverInfo("Body", "hsa-body", DateTime.Now));
+
+ var debugDir = Path.Combine(_dir, "debug");
+ var cfg = new AppConfig { TargetPrinter = "TestPrinter", DebugOutputDir = debugDir };
+ var audit = new AuditLogger(Path.Combine(_dir, "audit.jsonl"), Path.Combine(_dir, "error.log"));
+ var printer = new FakePrinter();
+
+ using (var temp = new SecureTempStore(Path.Combine(_dir, "tmp")))
+ {
+ new RemissWorkflow(cfg, printer, audit).Run(source, new Mottagning("Hud SU", "hsa-1"), temp);
+ }
+
+ Assert.Equal("TestPrinter", printer.Printer);
+ var debugCopies = Directory.GetFiles(debugDir, "remiss-debug-*.pdf");
+ Assert.Single(debugCopies);
+
+ using var merged = PdfSharp.Pdf.IO.PdfReader.Open(debugCopies[0], PdfSharp.Pdf.IO.PdfDocumentOpenMode.Import);
+ Assert.Equal(2, merged.PageCount); // försättsblad + "remiss"
+
+ var auditText = File.ReadAllText(Path.Combine(_dir, "audit.jsonl"));
+ Assert.Contains("remiss.sent", auditText);
+ }
+
+ [Fact]
+ public void Run_propagatesPrinterFailureAndAuditsTheStage()
+ {
+ var source = Path.Combine(_dir, "remiss.pdf");
+ CoverPageGenerator.Generate(source, new CoverInfo("Body", "hsa-body", DateTime.Now));
+
+ var cfg = new AppConfig { TargetPrinter = "X" };
+ var audit = new AuditLogger(Path.Combine(_dir, "audit.jsonl"), Path.Combine(_dir, "error.log"));
+
+ var boom = new ThrowingPrinter();
+ using var temp = new SecureTempStore(Path.Combine(_dir, "tmp"));
+
+ Assert.Throws<InvalidOperationException>(() =>
+ new RemissWorkflow(cfg, boom, audit).Run(source, new Mottagning("Hud SU", "hsa-1"), temp));
+
+ var auditText = File.ReadAllText(Path.Combine(_dir, "audit.jsonl"));
+ Assert.Contains("remiss.failed", auditText);
+ Assert.Contains("\"stage\":\"print\"", auditText);
+ }
+
+ private sealed class ThrowingPrinter : IPrinter
+ {
+ public void Print(string pdfPath, string printerName) => throw new InvalidOperationException("no printer");
+ }
+}
diff --git a/tools/SumatraPDF-settings.txt b/tools/SumatraPDF-settings.txt
new file mode 100644
index 0000000..4cc5ee6
--- /dev/null
+++ b/tools/SumatraPDF-settings.txt
@@ -0,0 +1,94 @@
+# For documentation, see https://www.sumatrapdfreader.org/settings/settings3-5-1.html
+Theme = Light
+FixedPageUI [
+ TextColor = #000000
+ BackgroundColor = #ffffff
+ SelectionColor = #f5fc0c
+ WindowMargin = 2 4 2 4
+ PageSpacing = 4 4
+ InvertColors = false
+ HideScrollbars = false
+]
+ComicBookUI [
+ WindowMargin = 0 0 0 0
+ PageSpacing = 4 4
+ CbxMangaMode = false
+]
+ChmUI [
+ UseFixedPageUI = false
+]
+
+SelectionHandlers [
+]
+ExternalViewers [
+]
+
+ZoomLevels = 8.33 12.5 18 25 33.33 50 66.67 75 100 125 150 200 300 400 600 800 1000 1200 1600 2000 2400 3200 4800 6400
+ZoomIncrement = 0
+
+PrinterDefaults [
+ PrintScale = shrink
+]
+ForwardSearch [
+ HighlightOffset = 0
+ HighlightWidth = 15
+ HighlightColor = #6581ff
+ HighlightPermanent = false
+]
+Annotations [
+ HighlightColor = #ffff00
+ UnderlineColor = #00ff00
+ SquigglyColor = #ff00ff
+ StrikeOutColor = #ff0000
+ FreeTextColor =
+ FreeTextSize = 12
+ FreeTextBorderWidth = 1
+ TextIconColor =
+ TextIconType =
+ DefaultAuthor =
+]
+
+RememberOpenedFiles = true
+RememberStatePerDocument = true
+RestoreSession = true
+UiLanguage = sv
+EnableTeXEnhancements = false
+DefaultDisplayMode = automatic
+DefaultZoom = fit page
+Shortcuts [
+]
+EscToExit = false
+ReuseInstance = false
+ReloadModifiedDocuments = true
+
+MainWindowBackground = #80fff200
+FullPathInTitle = false
+ShowMenubar = true
+ShowToolbar = true
+ShowFavorites = false
+ShowToc = true
+NoHomeTab = false
+TocDy = 0
+SidebarDx = 0
+ToolbarSize = 18
+TabWidth = 300
+TreeFontSize = 0
+TreeFontWeightOffset = 0
+TreeFontName = automatic
+SmoothScroll = false
+ShowStartPage = true
+CheckForUpdates = true
+WindowState = 1
+WindowPos = 0 0 0 0
+UseTabs = true
+UseSysColors = false
+CustomScreenDPI = 0
+
+FileStates [
+]
+SessionData [
+]
+TimeOfLastUpdateCheck = 0 0
+OpenCountWeek = 817
+
+# Settings below are not recognized by the current version