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 */ }
+ }
+}