CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
146 lines
4.9 KiB
Go
146 lines
4.9 KiB
Go
package ocr
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
dmscrypto "gitea.perlbach24.de/scripte/nexarch/dms/internal/crypto"
|
|
"gitea.perlbach24.de/scripte/nexarch/dms/internal/testenv"
|
|
"gitea.perlbach24.de/scripte/nexarch/dms/internal/textcrypt"
|
|
)
|
|
|
|
type otherKEK struct{}
|
|
|
|
func (otherKEK) TenantKEK(context.Context, string) ([]byte, error) {
|
|
return bytes.Repeat([]byte{0x5A}, dmscrypto.KEKSize), nil
|
|
}
|
|
|
|
type rawPage struct {
|
|
page int
|
|
text string
|
|
enc []byte
|
|
}
|
|
|
|
func rawPages(t *testing.T, h *harness, revID string) []rawPage {
|
|
t.Helper()
|
|
rows, err := h.e.Pool.Query(context.Background(), `SELECT page, text, text_enc FROM revision_ocr_pages WHERE revision_id = $1 ORDER BY page`, revID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer rows.Close()
|
|
var out []rawPage
|
|
for rows.Next() {
|
|
var r rawPage
|
|
if err := rows.Scan(&r.page, &r.text, &r.enc); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func wrappedKey(t *testing.T, h *harness, revID string) []byte {
|
|
t.Helper()
|
|
var k []byte
|
|
if err := h.e.Pool.QueryRow(context.Background(), `SELECT text_wrapped_dek FROM revision_ocr WHERE revision_id = $1`, revID).Scan(&k); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return k
|
|
}
|
|
|
|
// FDN-22 AK1 und Pruefung 1: erkannter Text liegt nur verschluesselt in der Datenbank, die Auslieferung entschluesselt im Speicher.
|
|
func TestRecognisedTextIsStoredEncrypted(t *testing.T) {
|
|
h := newHarness(t)
|
|
ctx := context.Background()
|
|
eng := DefaultEngine()
|
|
lines := englishPage
|
|
img := renderReference(t, eng, lines, 200)
|
|
docID, revID, err := h.upload(t, "scan.png", "image/png", pngBytes(t, img), false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st := h.drain(t, revID, h.processor("w1", nil)); st.Status != StatusReady {
|
|
t.Fatalf("status %s (%v)", st.Status, st.Error)
|
|
}
|
|
rows := rawPages(t, h, revID)
|
|
if len(rows) != 1 || rows[0].text != "" || len(rows[0].enc) == 0 {
|
|
t.Fatalf("rohzeilen: %+v", rows)
|
|
}
|
|
if len(wrappedKey(t, h, revID)) == 0 {
|
|
t.Fatal("kein verpackter textschluessel abgelegt")
|
|
}
|
|
word := "musterhandel"
|
|
if strings.Contains(strings.ToLower(string(rows[0].enc)), word) {
|
|
t.Fatalf("klartextwort %q im chiffretext", word)
|
|
}
|
|
pages, err := Pages(ctx, h.e.Pool, h.e.Crypto, testenv.TenantSlug, revID)
|
|
if err != nil || len(pages) != 1 || !strings.Contains(strings.ToLower(pages[0].Text), word) {
|
|
t.Fatalf("entschluesselte seite: %+v %v", pages, err)
|
|
}
|
|
text, err := (TextStore{Pool: h.e.Pool, Crypto: h.e.Crypto, TenantSlug: testenv.TenantSlug}).TextOf(ctx, docID)
|
|
if err != nil || !strings.Contains(strings.ToLower(text), word) {
|
|
t.Fatalf("textstore: %q %v", text, err)
|
|
}
|
|
if l, ok, err := RevisionLanguage(ctx, h.e.Pool, revID); err != nil || !ok || l.Code != "en" {
|
|
t.Errorf("sprache aus dem verschluesselten text nicht bestimmt: %+v %v %v", l, ok, err)
|
|
}
|
|
}
|
|
|
|
// AK2: fremder KEK und fehlender Dienst liefern keinen Klartext.
|
|
func TestRecognisedTextFailsClosedWithoutKey(t *testing.T) {
|
|
h := newHarness(t)
|
|
ctx := context.Background()
|
|
img := renderReference(t, DefaultEngine(), referencePages[0], 200)
|
|
docID, revID, err := h.upload(t, "scan.png", "image/png", pngBytes(t, img), false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h.drain(t, revID, h.processor("w1", nil))
|
|
|
|
foreign := dmscrypto.NewService(otherKEK{})
|
|
if _, err := Pages(ctx, h.e.Pool, foreign, testenv.TenantSlug, revID); !errors.Is(err, textcrypt.ErrDecryptText) {
|
|
t.Errorf("fremder kek: %v", err)
|
|
}
|
|
if _, err := Pages(ctx, h.e.Pool, nil, testenv.TenantSlug, revID); !errors.Is(err, textcrypt.ErrNoCrypto) {
|
|
t.Errorf("ohne dienst: %v", err)
|
|
}
|
|
if text, err := (TextStore{Pool: h.e.Pool}).TextOf(ctx, docID); err == nil || text != "" {
|
|
t.Errorf("textstore ohne dienst: %q %v", text, err)
|
|
}
|
|
}
|
|
|
|
// Ein bewusstes Duplikat uebernimmt den Text und verschluesselt ihn neu (eigener Schluessel, Bindung an die neue Revision).
|
|
func TestReusedTextIsReEncryptedForNewRevision(t *testing.T) {
|
|
h := newHarness(t)
|
|
ctx := context.Background()
|
|
img := renderReference(t, DefaultEngine(), referencePages[0], 200)
|
|
data := pngBytes(t, img)
|
|
_, rev1, err := h.upload(t, "a.png", "image/png", data, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h.drain(t, rev1, h.processor("w1", nil))
|
|
_, rev2, err := h.upload(t, "b.png", "image/png", data, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st := h.drain(t, rev2, h.processor("w1", nil)); st.Status != StatusReady {
|
|
t.Fatalf("status %s", st.Status)
|
|
}
|
|
if bytes.Equal(wrappedKey(t, h, rev1), wrappedKey(t, h, rev2)) {
|
|
t.Fatal("beide revisionen teilen sich den textschluessel")
|
|
}
|
|
a, b := rawPages(t, h, rev1), rawPages(t, h, rev2)
|
|
if len(a) != 1 || len(b) != 1 || b[0].text != "" || bytes.Equal(a[0].enc, b[0].enc) {
|
|
t.Fatalf("roh: %+v / %+v", a, b)
|
|
}
|
|
p1, _ := Pages(ctx, h.e.Pool, h.e.Crypto, testenv.TenantSlug, rev1)
|
|
p2, err := Pages(ctx, h.e.Pool, h.e.Crypto, testenv.TenantSlug, rev2)
|
|
if err != nil || len(p2) != 1 || p2[0].Text != p1[0].Text || p2[0].Source != SourceReused {
|
|
t.Fatalf("uebernommener text: %+v %v", p2, err)
|
|
}
|
|
}
|