Files
patrickandClaude Sonnet 5 de374d0986
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (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
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
UX-11: verwaltungsoberflächen für regeln, postfächer, klärungskorb und e-rechnung
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:59:48 +02:00

169 lines
5.9 KiB
Go

package einvoiceapi
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/downloadapi"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/einvoice"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/jobqueue"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/testenv"
)
func createDocument(t *testing.T, ctx context.Context, e *testenv.Env, createdBy string) (documentID, revisionID string) {
t.Helper()
if err := e.Pool.QueryRow(ctx, `INSERT INTO documents (title, created_by) VALUES ('Rechnung.pdf', $1) RETURNING id`, createdBy).Scan(&documentID); err != nil {
t.Fatal(err)
}
if err := e.Pool.QueryRow(ctx, `
INSERT INTO file_revisions (document_id, revision_number, storage_key, checksum_sha256, size_bytes, mime_type, created_by)
VALUES ($1, 1, 'k', 'h', 10, 'application/pdf', $2) RETURNING id
`, documentID, createdBy).Scan(&revisionID); err != nil {
t.Fatal(err)
}
return documentID, revisionID
}
func newTestServer(t *testing.T, e *testenv.Env, enqueuer *einvoice.Enqueuer) *httptest.Server {
t.Helper()
mux := http.NewServeMux()
NewServer(e.Pool, enqueuer, &downloadapi.DocumentOwnerChecker{Pool: e.Pool}).RegisterRoutes(mux)
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
return srv
}
func get(t *testing.T, srv *httptest.Server, path string) (int, map[string]any) {
t.Helper()
resp, err := http.Get(srv.URL + path)
if err != nil {
t.Fatal(err)
}
defer func() { _ = resp.Body.Close() }()
var out map[string]any
_ = json.NewDecoder(resp.Body).Decode(&out)
return resp.StatusCode, out
}
// Akzeptanzkriterium 3: das Ergebnis zeigt Status, die Stufen getrennt,
// Fehlerliste, Regelwerk- und Validator-Version.
func TestGetResult_SeparatesReadFromConformance(t *testing.T) {
e := testenv.New(t)
ctx := t.Context()
docID, revID := createDocument(t, ctx, e, e.UserID)
if _, err := e.Pool.Exec(ctx, `
INSERT INTO revision_einvoice (revision_id, format, container, status, findings, search_text)
VALUES ($1, 'ubl-invoice', 'xml', 'valid', '[]'::jsonb, 'RE-1')
`, revID); err != nil {
t.Fatal(err)
}
if _, err := e.Pool.Exec(ctx, `
INSERT INTO revision_einvoice_conformance (revision_id, status, ruleset_version, scenario, validator_version, report_sha256, reason, messages)
VALUES ($1, 'non_conformant', 'xrechnung-3.0.2', 'basic', 'kosit-1.6.3', 'deadbeef', '', '[{"code":"BR-01","message":"Pflichtfeld fehlt"}]'::jsonb)
`, revID); err != nil {
t.Fatal(err)
}
srv := newTestServer(t, e, nil)
status, out := get(t, srv, "/documents/"+docID+"/revisions/"+revID+"/einvoice?caller="+e.UserID)
if status != http.StatusOK {
t.Fatalf("lesen: %d %v", status, out)
}
read, _ := out["read"].(map[string]any)
if read == nil || read["status"] != "valid" {
t.Errorf("lese-ergebnis fehlt oder falsch: %v", out)
}
conf, _ := out["conformance"].(map[string]any)
if conf == nil {
t.Fatalf("konformitaet fehlt: %v", out)
}
if conf["status"] != "non_conformant" {
t.Errorf("konformitaetsstatus falsch: %v", conf)
}
if conf["validator_version"] != "kosit-1.6.3" || conf["ruleset_version"] != "xrechnung-3.0.2" {
t.Errorf("regelwerk-/validator-version fehlt: %v", conf)
}
msgs, _ := conf["messages"].([]any)
if len(msgs) != 1 {
t.Errorf("fehlerliste falsch: %v", conf)
}
if _, hasStage := out["status"]; hasStage {
t.Error("es darf kein zusammengefasstes \"gueltig\" ohne stufentrennung geben")
}
}
func TestGetResult_UnknownRevisionIs404(t *testing.T) {
e := testenv.New(t)
docID, revID := createDocument(t, t.Context(), e, e.UserID)
srv := newTestServer(t, e, nil)
status, _ := get(t, srv, "/documents/"+docID+"/revisions/"+revID+"/einvoice?caller="+e.UserID)
if status != http.StatusNotFound {
t.Errorf("ohne ergebnis: %d, want 404", status)
}
}
// Rechtematrix (Pruefung 1): ein fremder Nutzer bekommt 403 auf das
// Ergebnis eines Dokuments, das ihm nicht gehoert.
func TestGetResult_ForeignUserForbidden(t *testing.T) {
e := testenv.New(t)
ctx := t.Context()
docID, revID := createDocument(t, ctx, e, e.UserID)
if _, err := e.Pool.Exec(ctx, `
INSERT INTO revision_einvoice (revision_id, format, container, status, findings, search_text)
VALUES ($1, 'ubl-invoice', 'xml', 'valid', '[]'::jsonb, '')
`, revID); err != nil {
t.Fatal(err)
}
srv := newTestServer(t, e, nil)
status, _ := get(t, srv, "/documents/"+docID+"/revisions/"+revID+"/einvoice?caller=fremder-nutzer")
if status != http.StatusForbidden {
t.Errorf("fremder nutzer: %d, want 403", status)
}
status, _ = get(t, srv, "/documents/"+docID+"/revisions/"+revID+"/einvoice")
if status != http.StatusUnauthorized {
t.Errorf("ohne identitaet: %d, want 401", status)
}
}
// Akzeptanzkriterium 3: die Neuprüfung ist ausloesbar.
func TestRecheck_EnqueuesAndRejectsWithoutEnqueuer(t *testing.T) {
e := testenv.New(t)
ctx := t.Context()
docID, revID := createDocument(t, ctx, e, e.UserID)
if _, err := e.Pool.Exec(ctx, `
UPDATE file_revisions SET mime_type = 'application/pdf' WHERE id = $1
`, revID); err != nil {
t.Fatal(err)
}
srv := newTestServer(t, e, nil)
status, resp := get2(t, srv, http.MethodPost, "/documents/"+docID+"/revisions/"+revID+"/einvoice/recheck?caller="+e.UserID)
if status != http.StatusServiceUnavailable {
t.Errorf("ohne enqueuer: %d %v, want 503", status, resp)
}
queue := jobqueue.NewQueue(e.Pool, 0)
srvWithQueue := newTestServer(t, e, &einvoice.Enqueuer{Pool: e.Pool, Queue: queue})
status, resp = get2(t, srvWithQueue, http.MethodPost, "/documents/"+docID+"/revisions/"+revID+"/einvoice/recheck?caller="+e.UserID)
if status != http.StatusAccepted {
t.Fatalf("neupruefung einreihen: %d %v", status, resp)
}
}
func get2(t *testing.T, srv *httptest.Server, method, path string) (int, map[string]any) {
t.Helper()
req, _ := http.NewRequest(method, srv.URL+path, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer func() { _ = resp.Body.Close() }()
var out map[string]any
_ = json.NewDecoder(resp.Body).Decode(&out)
return resp.StatusCode, out
}