foxygit / digitalskrivare_merge Log in
commit 6ac2e22c598273e51df62ecb4407afa08329975b
Author:     Jens Kristoffersson <jens.kristoffersson.se@gmail.com>
AuthorDate: Tue Sep 1 09:59:13 2026 +0200
Commit:     Jens Kristoffersson <jens.kristoffersson.se@gmail.com>
CommitDate: Tue Sep 1 09:59:13 2026 +0200

    Lägg till Core-tester och verifiera bygget

    xUnit-projekt (tests/Remiss.Core.Tests) som täcker försättsblads-
    generering, PDF-merge, JSON-registret och SecureTempStore-städningen.
    6/6 gröna. Solution bygger rent (0 varningar) med .NET SDK 8.0.424.
    README uppdaterad: SDK-läge, teststatus, kända begränsningar.
---
 README.md                                        | 17 +++++--
 Remiss.sln                                       | 13 ++++-
 tests/Remiss.Core.Tests/PdfPipelineTests.cs      | 53 ++++++++++++++++++++
 tests/Remiss.Core.Tests/Remiss.Core.Tests.csproj | 21 ++++++++
 tests/Remiss.Core.Tests/SupportTests.cs          | 61 ++++++++++++++++++++++++
 5 files changed, 160 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md
index 11a5f83..693a015 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ Se [Plan.md](Plan.md) för arkitektur, teknikval och faser.
 | Fas | Innehåll | Läge |
 |-----|----------|------|
 | 0 | Spika hur nedströms-skrivaren läser av mottagning | **Blockerad – ej påbörjad** |
-| 1 | Fristående C#-app, körs manuellt med test-PDF | **Scaffold klart – detta repo** |
+| 1 | Fristående C#-app, körs manuellt med test-PDF | **Bygger + tester gröna. Kvar: manuellt dialog+utskriftstest.** |
 | 2 | Koppla på fångst (clawPDF) | Ej påbörjad |
 | 3 | Härdning (HSA, config, audit, temp, felflöden) | Delvis förberedd i koden |
 | 4 | Paketering & utrullning | Ej påbörjad |
@@ -20,8 +20,11 @@ Se [Plan.md](Plan.md) för arkitektur, teknikval och faser.
 ## Förutsättningar

 - Windows
-- **.NET 8 SDK** – finns _inte_ på den här maskinen ännu (bara runtimes).
-  Installera via Software Center eller <https://aka.ms/dotnet/download>.
+- **.NET SDK 8.x** (byggt och verifierat med 8.0.424). På utvecklingsmaskinen är
+  den installerad per användare i `%LOCALAPPDATA%\Microsoft\dotnet` – eftersom
+  maskin-PATH pekar på en SDK-lös runtime i `C:\Program Files\dotnet` finns en
+  `dotnet`-shim i PowerShell-profilen som väljer rätt host. `net8.0`-targeten
+  fungerar även med nyare SDK (10.x).
 - SumatraPDF-binär i `tools/` – se [tools/README.md](tools/README.md).
   Behövs bara för det sista utskriftssteget; dialog + försättsblad + merge
   fungerar utan.
@@ -30,6 +33,7 @@ Se [Plan.md](Plan.md) för arkitektur, teknikval och faser.

 ```powershell
 dotnet build Remiss.sln
+dotnet test  Remiss.sln

 # Kör mot en test-PDF och en testskrivare
 dotnet run --project src/Remiss.App -- "C:\temp\testremiss.pdf"
@@ -63,6 +67,8 @@ src/
   Remiss.App/              WinForms-exe (AssemblyName: Remiss)
     Program.cs             entrypoint, argument, returkoder
     MottagningDialog.cs    sökbar lista, avbryt-hantering
+tests/
+  Remiss.Core.Tests/       xUnit: PDF-pipeline, register, SecureTempStore
 ```

 ## Konfiguration (`config/appsettings.json`)
@@ -96,5 +102,8 @@ Remiss-PDF:erna är känsliga personuppgifter.
 - Försättsbladets layout är preliminär tills Fas 0 är löst.
 - Mottagningsregistret är statisk JSON; HSA-katalog kopplas i Fas 3 bakom
   `IMottagningRepository`.
-- Inga automatiska tester än.
+- Tester täcker Core (PDF, register, temp). Dialog + utskrift är inte
+  automattestade – kräver ett manuellt körtest.
+- Tidiga fel visas som blockerande `MessageBox`. OK för Fas 2 (användaren står
+  vid skärmen efter Skriv ut), men ses över inför tyst drift.
 - SecureFile.Shred är bäst-möjligt, inte forensiskt (SSD/wear-leveling).
diff --git a/Remiss.sln b/Remiss.sln
index 9c4e2b0..96df4fb 100644
--- a/Remiss.sln
+++ b/Remiss.sln
@@ -1,4 +1,4 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio Version 17
 VisualStudioVersion = 17.0.31903.59
 MinimumVisualStudioVersion = 10.0.40219.1
@@ -6,6 +6,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Remiss.Core", "src\Remiss.C
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Remiss.App", "src\Remiss.App\Remiss.App.csproj", "{22222222-2222-4222-8222-222222222222}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{98039F5D-2EEF-48E3-9B66-B40FE8971177}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Remiss.Core.Tests", "tests\Remiss.Core.Tests\Remiss.Core.Tests.csproj", "{8CE7B5D2-E03B-44A3-9D0B-CF114500A874}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -20,8 +24,15 @@ Global
 		{22222222-2222-4222-8222-222222222222}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{22222222-2222-4222-8222-222222222222}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{22222222-2222-4222-8222-222222222222}.Release|Any CPU.Build.0 = Release|Any CPU
+		{8CE7B5D2-E03B-44A3-9D0B-CF114500A874}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{8CE7B5D2-E03B-44A3-9D0B-CF114500A874}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{8CE7B5D2-E03B-44A3-9D0B-CF114500A874}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{8CE7B5D2-E03B-44A3-9D0B-CF114500A874}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
 	EndGlobalSection
+	GlobalSection(NestedProjects) = preSolution
+		{8CE7B5D2-E03B-44A3-9D0B-CF114500A874} = {98039F5D-2EEF-48E3-9B66-B40FE8971177}
+	EndGlobalSection
 EndGlobal
diff --git a/tests/Remiss.Core.Tests/PdfPipelineTests.cs b/tests/Remiss.Core.Tests/PdfPipelineTests.cs
new file mode 100644
index 0000000..351848f
--- /dev/null
+++ b/tests/Remiss.Core.Tests/PdfPipelineTests.cs
@@ -0,0 +1,53 @@
+using PdfSharp.Pdf.IO;
+using Remiss.Core.Pdf;
+using Xunit;
+
+namespace Remiss.Core.Tests;
+
+public sealed class PdfPipelineTests : IDisposable
+{
+    private readonly string _dir = Path.Combine(Path.GetTempPath(), "remiss-tests-" + Guid.NewGuid().ToString("N"));
+
+    public PdfPipelineTests() => Directory.CreateDirectory(_dir);
+
+    public void Dispose()
+    {
+        try { Directory.Delete(_dir, recursive: true); } catch { /* ignore */ }
+    }
+
+    [Fact]
+    public void CoverPageGenerator_producesReadableSinglePagePdf()
+    {
+        var path = Path.Combine(_dir, "cover.pdf");
+
+        CoverPageGenerator.Generate(path, new CoverInfo("Hudmottagningen Sahlgrenska", "SE2321000131-E000000000001", DateTime.Now));
+
+        Assert.True(File.Exists(path));
+        using var doc = PdfReader.Open(path, PdfDocumentOpenMode.Import);
+        Assert.Equal(1, doc.PageCount);
+    }
+
+    [Fact]
+    public void PdfMerger_concatenatesPagesInOrder()
+    {
+        var cover = Path.Combine(_dir, "cover.pdf");
+        var body = Path.Combine(_dir, "body.pdf");
+        var merged = Path.Combine(_dir, "merged.pdf");
+
+        CoverPageGenerator.Generate(cover, new CoverInfo("A", "hsa-a", DateTime.Now));
+        // Återanvänd generatorn för att skapa en 1-sidig "remiss".
+        CoverPageGenerator.Generate(body, new CoverInfo("B", "hsa-b", DateTime.Now));
+
+        PdfMerger.Merge(merged, cover, body);
+
+        using var doc = PdfReader.Open(merged, PdfDocumentOpenMode.Import);
+        Assert.Equal(2, doc.PageCount);
+    }
+
+    [Fact]
+    public void PdfMerger_throwsWhenSourceMissing()
+    {
+        var merged = Path.Combine(_dir, "merged.pdf");
+        Assert.Throws<FileNotFoundException>(() => PdfMerger.Merge(merged, Path.Combine(_dir, "nope.pdf")));
+    }
+}
diff --git a/tests/Remiss.Core.Tests/Remiss.Core.Tests.csproj b/tests/Remiss.Core.Tests/Remiss.Core.Tests.csproj
new file mode 100644
index 0000000..06caea5
--- /dev/null
+++ b/tests/Remiss.Core.Tests/Remiss.Core.Tests.csproj
@@ -0,0 +1,21 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net8.0-windows</TargetFramework>
+    <Nullable>enable</Nullable>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <IsPackable>false</IsPackable>
+    <IsTestProject>true</IsTestProject>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
+    <PackageReference Include="xunit" Version="2.9.2" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\Remiss.Core\Remiss.Core.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/tests/Remiss.Core.Tests/SupportTests.cs b/tests/Remiss.Core.Tests/SupportTests.cs
new file mode 100644
index 0000000..f01ecf1
--- /dev/null
+++ b/tests/Remiss.Core.Tests/SupportTests.cs
@@ -0,0 +1,61 @@
+using Remiss.Core.Repositories;
+using Remiss.Core.Security;
+using Xunit;
+
+namespace Remiss.Core.Tests;
+
+public sealed class SupportTests
+{
+    [Fact]
+    public void JsonMottagningRepository_readsSortsAndFiltersRegistry()
+    {
+        var path = Path.Combine(Path.GetTempPath(), "mottagningar-" + Guid.NewGuid().ToString("N") + ".json");
+        File.WriteAllText(path, """
+        [
+          { "namn": "Öron Kungälv", "hsaId": "hsa-3" },
+          { "namn": "Hud SU",       "hsaId": "hsa-1" },
+          { "namn": "",             "hsaId": "hsa-x" },
+          { "namn": "Ortopedi",     "hsaId": "hsa-2" }
+        ]
+        """);
+
+        try
+        {
+            var repo = new JsonMottagningRepository(path);
+            var all = repo.GetAll();
+
+            Assert.Equal(3, all.Count);                       // tom rad bortfiltrerad
+            Assert.Equal("Hud SU", all[0].Namn);              // sorterad på namn
+            Assert.Equal("hsa-2", repo.FindByHsaId("HSA-2")!.HsaId); // skiftlägesokänsligt
+            Assert.Null(repo.FindByHsaId("saknas"));
+        }
+        finally
+        {
+            File.Delete(path);
+        }
+    }
+
+    [Fact]
+    public void JsonMottagningRepository_throwsWhenFileMissing()
+    {
+        Assert.Throws<FileNotFoundException>(() =>
+            new JsonMottagningRepository(Path.Combine(Path.GetTempPath(), "does-not-exist.json")));
+    }
+
+    [Fact]
+    public void SecureTempStore_shredsAndRemovesEverythingOnDispose()
+    {
+        var baseDir = Path.Combine(Path.GetTempPath(), "remiss-securetmp-" + Guid.NewGuid().ToString("N"));
+        string root;
+
+        using (var store = new SecureTempStore(baseDir))
+        {
+            root = store.Root;
+            File.WriteAllText(store.NewFile("patient.pdf"), "känsligt innehåll");
+            Assert.True(File.Exists(store.NewFile("patient.pdf")));
+        }
+
+        Assert.False(Directory.Exists(root));
+        try { Directory.Delete(baseDir, recursive: true); } catch { /* ignore */ }
+    }
+}