QA-01: Teststrategie Connect #218
No files matched your search
@@ -29,6 +29,18 @@ jobs:
|
||||
working-directory: dms
|
||||
run: bash scripts/check-security-test-coverage.sh
|
||||
|
||||
connect-security-gate:
|
||||
name: Connect Security-Test-Gate (QA-01 AC4, Merge-Gate)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Gate-Selbsttest (beweist, dass fehlende Tests abgelehnt werden)
|
||||
working-directory: connect
|
||||
run: bash scripts/check-security-test-coverage_selftest.sh
|
||||
- name: Gate gegen aktuellen Stand ausfuehren
|
||||
working-directory: connect
|
||||
run: bash scripts/check-security-test-coverage.sh
|
||||
|
||||
dms-build-test:
|
||||
name: DMS-Modul (Lint/Build/Test/Coverage/Artefakt)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Command gen-test-corpus erzeugt den QA-01-Referenzkorpus unter
|
||||
// testdata/corpus/. Aufruf aus connect/ heraus: go run ./cmd/gen-test-corpus
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/testcorpus"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := testcorpus.Generate("testdata/corpus"); err != nil {
|
||||
log.Fatalf("gen-test-corpus: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
# QA-01 – Prüfprotokoll: Teststrategie Connect
|
||||
|
||||
Welle 1. Keine Vorbedingungen.
|
||||
|
||||
## Umsetzung
|
||||
|
||||
- `connect/docs/QA-01-TESTSTRATEGIE.md` – Testpyramide (Anteile +
|
||||
Begruendung fuer diesen Stack), Testarten je Ausbaustufe, Freigabe-
|
||||
kriterien je Prüfgate, verbindliche Security-Merge-Gate-Regel
|
||||
(Akzeptanzkriterium 1, 3, 4).
|
||||
- `connect/scripts/check-security-test-coverage.sh` +
|
||||
`_selftest.sh` – Mechanismus 1:1 aus `dms/scripts/...` uebernommen (kein
|
||||
neuer Mechanismus, kein Umbau), Paketliste fuer Connect neu ermittelt
|
||||
(`grep -rl "TenantSlug\|TenantID" internal --include=*.go` + manuelle
|
||||
Pruefung Krypto-/Autorisierungs-Naehe): 15 Pakete.
|
||||
- `.gitea/workflows/ci.yml` – neuer Job `connect-security-gate`
|
||||
(Selbsttest + Gate-Lauf), analog `dms-security-gate` (Akzeptanzkriterium
|
||||
4).
|
||||
- `connect/internal/crypto/envelope_test.go`,
|
||||
`connect/internal/auditclient/auditclient_test.go`,
|
||||
`connect/internal/policyclient/policyclient_test.go` – bei Einfuehrung
|
||||
des Gates real fehlende Tests fuer drei sicherheitsrelevante Pakete
|
||||
(Verschluesselung, Audit-Meldung, Rechteentscheidung) nachgeliefert;
|
||||
ohne diese drei Dateien haette das neu eingefuehrte Gate den aktuellen
|
||||
Stand sofort abgelehnt (echter, in dieser Kachel behobener Befund).
|
||||
- `connect/internal/testcorpus/` – Generator fuer den Referenzkorpus
|
||||
(ERP, CRM, DATEV, Microsoft Graph), vollstaendig mit Go-Standard-
|
||||
bibliothek, keine neue Abhaengigkeit in `go.mod`.
|
||||
- `connect/cmd/gen-test-corpus/main.go` – CLI-Einstiegspunkt,
|
||||
`go run ./cmd/gen-test-corpus` aus `connect/` heraus.
|
||||
- `connect/testdata/corpus/` – erzeugte Beispieldateien, mitcommitted
|
||||
(Akzeptanzkriterium 2).
|
||||
- `connect/internal/testcorpus/testcorpus_test.go` – prueft, dass alle
|
||||
vier Systemtypen im generierten Korpus vorkommen, dass jede Datei fuer
|
||||
ihren Typ gueltig strukturiert ist (JSON-Pflichtfeld je Typ bzw.
|
||||
EXTF-Kopfzeile fuer DATEV) und dass wiederholte Laeufe denselben Inhalt
|
||||
liefern (Reproduzierbarkeit).
|
||||
|
||||
## Warum kein automatisierter Test fuer die Strategie-Datei selbst
|
||||
|
||||
`QA-01-TESTSTRATEGIE.md` ist ein Prosa-Dokument (Festlegung, kein
|
||||
ausfuehrbarer Code) – ein automatisierter Test dafuer waere Formsache ohne
|
||||
echten Pruefwert. Stattdessen technisch verankert: der Merge-Gate-
|
||||
Mechanismus hat einen Beweistest (`check-security-test-coverage_selftest.sh`),
|
||||
der Korpus-Generator hat einen echten Test (`testcorpus_test.go`), und
|
||||
jedes der drei nachgelieferten Sicherheitspakete hat jetzt eigene,
|
||||
inhaltliche Tests (nicht nur Formsache: Rundtrip, Ablehnung bei falschem
|
||||
Tenant, Ablehnung bei manipuliertem Ciphertext, Fail-Closed bei
|
||||
Transport-/Statusfehlern). Damit ist jedes ausfuehrbare Artefakt dieser
|
||||
Kachel getestet, das Strategiedokument bewusst nicht.
|
||||
|
||||
## Prüfungen vor der Abnahme
|
||||
|
||||
| # | Prüfung | Zielwert | Ergebnis |
|
||||
|---|---|---|---|
|
||||
| 1 | Teststrategie-Dokument liegt vollstaendig vor und wurde geprueft | Dokument deckt Testpyramide, Testarten je Ausbaustufe und Freigabekriterien ab, anwendbar fuer alle Connect-Epics | **bestanden** – `connect/docs/QA-01-TESTSTRATEGIE.md` enthaelt alle drei Abschnitte, verweist auf ERP/CRM/DATEV/M365 als Epics gleichermassen, keine epicspezifische Engfuehrung |
|
||||
| 2 | Testkorpus im Test auf Vollstaendigkeit je Systemtyp geprueft | Alle vier geforderten Systemtypen (ERP, CRM, DATEV, Microsoft Graph) vorhanden und je Typ strukturell gueltig | **bestanden** – `TestGenerate_AllSystemTypesCoveredAndParseable` (siehe Testlauf unten) prueft automatisiert Vorhandensein + Pflichtfeld je Typ; zusaetzlich manuell mit `file` bestaetigt (siehe Testlauf) |
|
||||
| 3 | Freigabekriterien im Test auf Eindeutigkeit geprueft | Jedes Freigabekriterium ist als eindeutige, ueberpruefbare Bedingung formuliert (kein "sollte", kein Ermessensspielraum) | **bestanden** – manuelle Durchsicht der vier Freigabekriterien in `QA-01-TESTSTRATEGIE.md`: jedes benennt eine konkrete, technisch pruefbare Bedingung (abgelegtes Protokoll vorhanden ja/nein, realer Korpus-/Testsystembezug vorhanden ja/nein, Gate-Skript-Exitcode, Fehlerinjektionsnachweis vorhanden ja/nein) statt vager Formulierung. Zusaetzlich technisch bestaetigt fuer Freigabekriterium 3 (Merge-Gate) durch den Beweistest des Gates selbst (siehe Testlauf) |
|
||||
|
||||
## Build/Test-Ergebnis (192.168.1.204, Wurf-Verzeichnis
|
||||
`/root/nexarch-forks/qa-01-connect/connect`, per rsync aus lokalem Klon
|
||||
`feature/qa-01-01-teststrategie-connect` synchronisiert)
|
||||
|
||||
```
|
||||
go build ./... -> clean
|
||||
go vet ./... -> clean
|
||||
go test ./... -count=1 -p 1 -> alle Pakete PASS
|
||||
(u.a. internal/crypto,
|
||||
internal/auditclient,
|
||||
internal/policyclient,
|
||||
internal/testcorpus neu)
|
||||
bash scripts/check-security-test-coverage_selftest.sh -> "Beweistest bestanden" (Exit 0);
|
||||
Fall 2 (Tests aus internal/crypto
|
||||
entfernt) lehnt korrekt ab
|
||||
bash scripts/check-security-test-coverage.sh -> "Merge-Gate bestanden" (Exit 0),
|
||||
15 Pakete geprueft
|
||||
go run ./cmd/gen-test-corpus -> 4 Dateien erzeugt (erp, crm, datev, m365)
|
||||
file testdata/corpus/*/* -> crm/erp/m365: "JSON text data",
|
||||
datev: "ASCII text, with CRLF
|
||||
line terminators"
|
||||
```
|
||||
|
||||
## DSGVO-Bestaetigung (fuer den Referenzkorpus)
|
||||
|
||||
Alle vier Korpus-Dateien sind vollstaendig synthetisch, in dieser Sitzung
|
||||
frei erfunden: fiktive Firma "Musterhandel Nordlicht GmbH", fiktive Person
|
||||
"Anna Testperson", fiktive E-Mail-Adresse unter der reservierten
|
||||
Beispieldomain `*.example` (RFC 2606), fiktive Debitoren-/Belegnummern,
|
||||
fiktive USt-ID (`DE999999999`, kein reales Format-Praefix einer
|
||||
existierenden Nummer), fiktive Betraege. Kein Inhalt wurde aus einer
|
||||
echten Kunden-/Drittsystem-Antwort, einem Export oder Altsystem
|
||||
uebernommen oder abgeleitet – uebernommen wurde ausschliesslich die
|
||||
*Struktur/Feldbenennung* der jeweiligen real dokumentierten Schnittstelle
|
||||
(HubSpot CRM v3, Microsoft Graph v1.0 `/users`, DATEV-EXTF-Format gemaess
|
||||
bereits in `internal/datevexport` implementierter Spezifikation, generische
|
||||
ERP-Belegstruktur analog `erpconnector`), nicht deren Inhalte. Das erfuellt
|
||||
die Ticket-Vorgabe "reale Drittsystem-Antworten" im Sinne von *real
|
||||
verwendete Schnittstellenschemata*, ohne gegen das Repo-Verbot echter
|
||||
Kunden-/Mandantendaten zu verstossen (`CLAUDE.md`: keine echten Kunden-/
|
||||
Mandantendaten in Testfixtures).
|
||||
|
||||
## Entscheidungen/Ermessen in dieser Sitzung
|
||||
|
||||
- **Merge-Gate-Paketliste**: 15 Pakete real per Tenant-Bezug/Krypto-/
|
||||
Autorisierungs-Naehe ermittelt (nicht geschaetzt) – siehe Kommandos
|
||||
oben in der Teststrategie. Drei davon (`crypto`, `auditclient`,
|
||||
`policyclient`) hatten noch keine Tests; da ein neu eingefuehrtes,
|
||||
aber sofort rot laufendes Gate keinen echten Nutzen haette (und dem
|
||||
Grundsatz "keine Freigabe auf Zuruf" widerspraeche, wenn man es einfach
|
||||
mit einer kleineren Liste "gruen faerbt"), wurden fuer diese drei
|
||||
Pakete kleinstmoegliche, inhaltlich echte Tests ergaenzt statt sie aus
|
||||
der Liste auszunehmen.
|
||||
- **"Reale Drittsystem-Antworten" ohne Produktivzugang**: da diese Sitzung
|
||||
keinen Zugang zu echten Kunden-ERP/CRM/DATEV/Microsoft-365-Systemen hat
|
||||
(und DSGVO das ohnehin ausschliesst), wurde "real" als "Struktur/Schema
|
||||
einer tatsaechlich eingesetzten, oeffentlich dokumentierten Schnittstelle"
|
||||
ausgelegt statt als "Antwort eines echten Kundensystems". Diese Auslegung
|
||||
ist im Abschnitt "DSGVO-Bestaetigung" oben begruendet; bei Bedarf vom
|
||||
Fachbereich gegenzupruefen.
|
||||
|
||||
## Gesamtergebnis
|
||||
|
||||
**Bestanden.** Alle vier Akzeptanzkriterien erfuellt: Teststrategie
|
||||
dokumentiert und epicuebergreifend anwendbar, Testkorpus mit allen vier
|
||||
geforderten Systemtypen vorhanden und automatisiert geprueft, Freigabe-
|
||||
kriterien eindeutig formuliert, Merge-Gate fuer sicherheitsrelevanten Code
|
||||
technisch verankert (inkl. Beweistest) und die dabei real gefundene Luecke
|
||||
(drei ungetestete Sicherheitspakete) in derselben Kachel geschlossen. Alle
|
||||
drei Prüfungen vor der Abnahme real durchgefuehrt und bestanden.
|
||||
@@ -0,0 +1,138 @@
|
||||
# QA-01 – Teststrategie Connect
|
||||
|
||||
Welle 1. Keine Vorbedingungen. Gilt fuer alle Connect-Epics (ERP/CRM-
|
||||
Anbindung, DATEV, Microsoft 365, DMS-Interoperabilitaet, externe
|
||||
Backup-Ziele) und liefert den Rahmen, auf dem `QA-02` (Connector-Engine)
|
||||
und `QA-03` (ERP/DATEV/M365) ihre eigenen, fachspezifischen Prüfgates
|
||||
aufbauen.
|
||||
|
||||
## Testpyramide
|
||||
|
||||
Stack: Go-Backend (`connect/`, eigenes Modul, Pakete unter `internal/` pro
|
||||
fachlichem Bereich), baut auf Core-Infrastruktur (REST-API, Rate-Limiting,
|
||||
Webhook-Zustellung, Token-Verwaltung, Audit-Log) statt eigener
|
||||
Infrastruktur.
|
||||
|
||||
| Ebene | Anteil (Zielbild) | Was hier getestet wird | Werkzeug |
|
||||
|---|---|---|---|
|
||||
| Unit | ca. 65% | Einzelne Funktionen/Methoden innerhalb eines `internal/`-Pakets: Feldabbildung (`fieldmapping`), Verschluesselung (`crypto`), DATEV-Formatbildung (`datevexport`), Zustandsuebergaenge in `syncjob`. Keine echte Drittsystem-Verbindung, kein echtes Netz noetig, wo Interfaces das zulassen. | `go test` |
|
||||
| Integration | ca. 25% | Zusammenspiel mehrerer Pakete inkl. echter, pro Lauf frisch aufgesetzter Testdatenbank sowie HTTP-Handler-Tests (`*api`-Pakete, z. B. `statusapi`, `datevapi`) mit echter Anfrage/Antwort ueber `httptest`. Drittsystem-Antworten werden dabei ueber den Referenzkorpus (s. u.) simuliert, nicht frei erfunden. | `go test` gegen Testdatenbank, `net/http/httptest` |
|
||||
| E2E | ca. 10% | Vollstaendiger Durchlauf eines Sync-Jobs gegen ein echtes, wenn auch als Testsystem markiertes Drittsystem (mind. ein realer Testlauf je Ausbaustufe, siehe Freigabekriterien). Bewusst hoeher als bei reinen Datenmodul-Tests (DMS), weil Connect fast ausschliesslich von der Korrektheit externer Schnittstellenvertraege lebt – ein Connector kann alle Unit-/Integrationstests bestehen und trotzdem am realen Drittsystemverhalten (Rate-Limits, Feldabweichungen, Zeitzonen) scheitern. | Manuelle/skriptgesteuerte Testlaeufe je Prüfgate-Kachel (QA-02/QA-03) |
|
||||
|
||||
Begruendung: Die Kernrisiken bei Connect liegen nicht in der eigenen
|
||||
Geschaeftslogik (die ist vergleichsweise einfach – Mapping, Zustandsmaschine,
|
||||
Verschluesselung), sondern im Vertrag mit Drittsystemen. Deshalb hoeherer
|
||||
E2E-Anteil als typische Datenmodule, aber dennoch klein gehalten (teuer,
|
||||
verlangt echte Testzugaenge), und durch den Referenzkorpus (Ausbaustufe 1)
|
||||
teilweise durch guenstigere Integrationstests ersetzt.
|
||||
|
||||
## Testarten je Ausbaustufe
|
||||
|
||||
Jede Ausbaustufe (Connector-Anlage bis Produktivbetrieb) verlangt eine
|
||||
zunehmend strengere Prüftiefe:
|
||||
|
||||
| Ausbaustufe | Pflicht-Testart | Freigabekriterium |
|
||||
|---|---|---|
|
||||
| 1. Skelett/Schema (Connector-Grundgeruest, Feldmapping) | Unit-Tests je Paket, Mapping-Validierung gegen den Referenzkorpus | Alle Unit-Tests gruen, Mapping deckt jedes Pflichtfeld des Korpus-Beispiels ab |
|
||||
| 2. Integration gegen Korpus | Integrationstest: Connector verarbeitet die echte Beispielantwort aus `testdata/corpus/<systemtyp>/` fehlerfrei (Parsing, Feldabbildung, Persistenz) | Verarbeitung des Korpus-Beispiels ohne Fehler, Ergebnis stimmt mit erwarteten Zielwerten ueberein |
|
||||
| 3. Fehlerinjektion | Simulierter Drittsystem-Ausfall (Verbindungsabbruch, Timeout, HTTP 5xx, unerwartete Antwortstruktur) gegen die Retry-/Fehlerbehandlungslogik | Retry-Verhalten greift nachweislich (kein stiller Datenverlust, kein Endlos-Retry, Fehler wird sichtbar protokolliert/audit-vermerkt) – **Freigabevoraussetzung fuer produktiven Einsatz der Retry-Logik**, siehe Produkt-DNA dieser Kachel |
|
||||
| 4. Realer Testlauf | Mindestens ein Testlauf gegen ein echtes (Test-)Drittsystem je angebundenem Systemtyp, nicht nur gegen Mock-/Korpus-Antworten | Realer Testlauf dokumentiert (Zeitpunkt, Zielsystem, Ergebnis) in der jeweiligen Prüfgate-Kachel (QA-02/QA-03) – ohne diesen Nachweis keine Freigabe fuer den produktiven Rollout dieser Ausbaustufe |
|
||||
|
||||
Ausbaustufen 3 und 4 sind bewusst nicht in dieser Kachel abschliessend
|
||||
implementiert (siehe "Nicht Bestandteil"), sondern als verbindliche
|
||||
Freigabekriterien fuer QA-02/QA-03 festgelegt.
|
||||
|
||||
## Freigabekriterien je Prüfgate
|
||||
|
||||
Damit "Freigabe auf Zuruf" technisch ausgeschlossen ist, gilt fuer jedes
|
||||
Connect-Prüfgate (diese wie auch nachfolgende Kacheln) einheitlich:
|
||||
|
||||
1. **Kein Prüfgate ohne abgelegtes Protokoll.** Jede Freigabe erfordert eine
|
||||
`*-PRUEFPROTOKOLL.md` mit Zielwert und tatsaechlichem Ergebnis je
|
||||
Prüfung – eine muendliche/Chat-Aussage "funktioniert" gilt nicht als
|
||||
Freigabe.
|
||||
2. **Kein Prüfgate nur gegen Mock-Antworten.** Mindestens ein Testlauf je
|
||||
Ausbaustufe muss entweder den Referenzkorpus (reale Drittsystem-
|
||||
Antwortstruktur, Ausbaustufe 1–2) oder ein echtes Testsystem
|
||||
(Ausbaustufe 3–4) verwenden – reine Freihand-Mocks ohne Bezug zu einer
|
||||
echten Schnittstelle reichen nicht.
|
||||
3. **Kein Merge auf sicherheitsrelevanten Code ohne Test.** Siehe
|
||||
Merge-Gate unten – technisch durchgesetzt, nicht nur dokumentiert.
|
||||
4. **Fehlerinjektion vor Freigabe der Retry-Logik.** Ein Connector-Prüfgate
|
||||
gilt fuer produktiven Einsatz erst als freigegeben, wenn mindestens ein
|
||||
nachgewiesener Fehlerfall (Ausfall/Timeout) gegen die Retry-Logik
|
||||
durchgefuehrt wurde (siehe Ausbaustufe 3).
|
||||
|
||||
## Pflichttests für sicherheitsrelevanten Code (verbindliches Merge-Gate)
|
||||
|
||||
Analog Core `QA-01`: Tenant-Scoping, Zugriffspruefung und Auth-nahe Logik
|
||||
sind kein optionales Extra, sondern Merge-Voraussetzung.
|
||||
|
||||
> Kein Merge von Code in einem als sicherheitsrelevant eingestuften Paket
|
||||
> ohne mindestens eine begleitende Testdatei mit mindestens einem
|
||||
> `func Test...`.
|
||||
|
||||
Technisch durchgesetzt ueber `connect/scripts/check-security-test-coverage.sh`
|
||||
(Mechanismus 1:1 aus `dms/scripts/check-security-test-coverage.sh`
|
||||
uebernommen, kein neuer Mechanismus), als `connect-security-gate`-Job in
|
||||
`.gitea/workflows/ci.yml` verankert. Paketliste (per
|
||||
`grep -rl "TenantSlug\|TenantID" internal --include=*.go` sowie manueller
|
||||
Pruefung auf Krypto-/Autorisierungs-Naehe ermittelt):
|
||||
|
||||
```
|
||||
internal/crypto (Verschluesselung ruhender Zugangsdaten)
|
||||
internal/credential (Zugangsdatenverwaltung)
|
||||
internal/auditclient (Audit-Meldung an Core)
|
||||
internal/audittrail (ausfalltoleranter Audit-Wrapper)
|
||||
internal/policyclient (Rechteentscheidung gegen Core)
|
||||
internal/archiveclient (Tenant-Bezug, externe Schnittstelle)
|
||||
internal/datevexport (Tenant-Bezug)
|
||||
internal/datevtransfer (Tenant-Bezug)
|
||||
internal/datevapi (Tenant-Scoping per X-Tenant-Slug, Handler)
|
||||
internal/dms2import (Tenant-Bezug)
|
||||
internal/dms2importapi (Tenant-Bezug, Handler)
|
||||
internal/fieldmapping (Tenant-Bezug)
|
||||
internal/masterdatasync (Tenant-Bezug)
|
||||
internal/syncjob (Tenant-Bezug)
|
||||
internal/statusapi (Tenant-Scoping per X-Tenant-Slug, Handler)
|
||||
```
|
||||
|
||||
Bei Einfuehrung: `internal/crypto`, `internal/auditclient` und
|
||||
`internal/policyclient` hatten trotz Sicherheitsrelevanz noch keine
|
||||
Testdatei (echter, in dieser Kachel behobener Befund – nicht nur
|
||||
dokumentiert, siehe Prüfprotokoll). Kuenftige neue Drittsystem-Connector-
|
||||
Pakete (z. B. Microsoft-Graph-Client) mit Tenant-Bezug sind bei ihrer
|
||||
Einfuehrung in dieselbe Liste aufzunehmen.
|
||||
|
||||
## Referenzkorpus
|
||||
|
||||
Siehe `connect/testdata/corpus/{erp,crm,datev,m365}/`, erzeugt durch
|
||||
`go run ./cmd/gen-test-corpus` aus `connect/internal/testcorpus`. Je
|
||||
angebundenem Drittsystemtyp eine Beispielantwort, deren Struktur/Feldnamen
|
||||
den real veroeffentlichten Schnittstellenschemata der jeweiligen
|
||||
Systemklasse entsprechen (Details und DSGVO-Erklaerung siehe
|
||||
`connect/docs/QA-01-PRUEFPROTOKOLL.md`):
|
||||
|
||||
- **ERP**: generische Belegantwort (Kopf + Positionen), wie sie
|
||||
`erpconnector` per REST liest/schreibt.
|
||||
- **CRM**: HubSpot-CRM-v3-Contact-Schema (real dokumentiert:
|
||||
`id` + `properties`-Map).
|
||||
- **DATEV**: EXTF-Buchungsstapel-Zeile exakt im Format, das
|
||||
`internal/datevexport` bereits produziert (Format-Kategorie 21).
|
||||
- **Microsoft Graph**: v1.0-Antwortform fuer `GET /users`
|
||||
(real dokumentiert: `@odata.context` + `value`-Array).
|
||||
|
||||
Dieser Korpus deckt Ausbaustufe 1–2 ab (Schema-/Integrationspruefung gegen
|
||||
reale Antwortformen). Ausbaustufe 3 (Fehlerinjektion) und 4 (realer
|
||||
Testlauf gegen ein echtes Testsystem) sind gemaess "Nicht Bestandteil"
|
||||
Aufgabe der jeweiligen Prüfgate-Kachel (QA-02/QA-03), die diesen Korpus als
|
||||
Ausgangsbasis weiterverwenden.
|
||||
|
||||
## Nicht Bestandteil dieser Kachel
|
||||
|
||||
Konkrete Prüfgates fuer die Connector-Engine (`QA-02`) sowie fuer
|
||||
ERP/DATEV/M365 (`QA-03`) sind eigene Kacheln. Diese Strategie liefert nur
|
||||
den Rahmen (Pyramide, Testarten je Ausbaustufe, Freigabekriterien, Merge-
|
||||
Gate) und den Referenzkorpus, den jene Kacheln als Eingabedaten sowie fuer
|
||||
ihre eigene Fehlerinjektion (Ausbaustufe 3) und ihren realen Testlauf
|
||||
(Ausbaustufe 4) wiederverwenden koennen.
|
||||
@@ -0,0 +1,57 @@
|
||||
package auditclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIngest_SendsTenantSlugAndServiceToken(t *testing.T) {
|
||||
var gotBody ingestRequest
|
||||
var gotToken string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotToken = r.Header.Get("X-Service-Token")
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||
t.Fatalf("body dekodieren: %v", err)
|
||||
}
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(srv.URL, "svc-token-123")
|
||||
err := c.Ingest(context.Background(), "acme", "user-1", "connector.sync", "erp:kunde:42", map[string]any{"count": 3})
|
||||
if err != nil {
|
||||
t.Fatalf("Ingest: %v", err)
|
||||
}
|
||||
|
||||
if gotToken != "svc-token-123" {
|
||||
t.Fatalf("Service-Token nicht gesendet, erhalten: %q", gotToken)
|
||||
}
|
||||
if gotBody.TenantSlug != "acme" {
|
||||
t.Fatalf("TenantSlug fehlt/falsch im Audit-Ereignis: %q", gotBody.TenantSlug)
|
||||
}
|
||||
if gotBody.Action != "connector.sync" {
|
||||
t.Fatalf("Action falsch: %q", gotBody.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngest_UnexpectedStatus_ReturnsError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(srv.URL, "svc-token-123")
|
||||
if err := c.Ingest(context.Background(), "acme", "user-1", "connector.sync", "erp:kunde:42", nil); err == nil {
|
||||
t.Fatal("erwarteter Fehler bei Status 500 blieb aus")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngest_TransportError_ReturnsError(t *testing.T) {
|
||||
c := New("http://127.0.0.1:1", "svc-token-123")
|
||||
if err := c.Ingest(context.Background(), "acme", "user-1", "connector.sync", "erp:kunde:42", nil); err == nil {
|
||||
t.Fatal("erwarteter Transport-Fehler blieb aus")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeKEKProvider struct {
|
||||
keys map[string][]byte
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeKEKProvider) TenantKEK(_ context.Context, tenantSlug string) ([]byte, error) {
|
||||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
kek, ok := f.keys[tenantSlug]
|
||||
if !ok {
|
||||
return nil, errors.New("fakeKEKProvider: unbekannter tenant")
|
||||
}
|
||||
return kek, nil
|
||||
}
|
||||
|
||||
func newFakeKEK(tenantSlug string) *fakeKEKProvider {
|
||||
kek := make([]byte, KEKSize)
|
||||
for i := range kek {
|
||||
kek[i] = byte(i) ^ byte(len(tenantSlug))
|
||||
}
|
||||
for i, c := range []byte(tenantSlug) {
|
||||
if i < len(kek) {
|
||||
kek[i] ^= c
|
||||
}
|
||||
}
|
||||
return &fakeKEKProvider{keys: map[string][]byte{tenantSlug: kek}}
|
||||
}
|
||||
|
||||
func TestSealOpen_RoundTrip(t *testing.T) {
|
||||
svc := NewService(newFakeKEK("acme"))
|
||||
plaintext := []byte("geheimes-api-token")
|
||||
|
||||
sealed, err := svc.Seal(context.Background(), "acme", plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Seal: %v", err)
|
||||
}
|
||||
if bytes.Contains(sealed.Ciphertext, plaintext) {
|
||||
t.Fatal("Ciphertext enthaelt Klartext")
|
||||
}
|
||||
|
||||
opened, err := svc.Open(context.Background(), "acme", sealed)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
if !bytes.Equal(opened, plaintext) {
|
||||
t.Fatalf("Open lieferte %q, erwartet %q", opened, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpen_WrongTenantKEK_Fails(t *testing.T) {
|
||||
svc := NewService(newFakeKEK("acme"))
|
||||
sealed, err := svc.Seal(context.Background(), "acme", []byte("secret"))
|
||||
if err != nil {
|
||||
t.Fatalf("Seal: %v", err)
|
||||
}
|
||||
|
||||
other := newFakeKEK("beta")
|
||||
svcOther := NewService(other)
|
||||
if _, err := svcOther.Open(context.Background(), "beta", sealed); !errors.Is(err, ErrDecryptFailed) {
|
||||
t.Fatalf("erwartet ErrDecryptFailed bei falschem KEK, erhalten: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeal_KEKUnavailable(t *testing.T) {
|
||||
svc := NewService(&fakeKEKProvider{err: errors.New("kek-dienst nicht erreichbar")})
|
||||
if _, err := svc.Seal(context.Background(), "acme", []byte("secret")); !errors.Is(err, ErrKEKUnavailable) {
|
||||
t.Fatalf("erwartet ErrKEKUnavailable, erhalten: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeal_TamperedCiphertext_FailsOnOpen(t *testing.T) {
|
||||
svc := NewService(newFakeKEK("acme"))
|
||||
sealed, err := svc.Seal(context.Background(), "acme", []byte("secret"))
|
||||
if err != nil {
|
||||
t.Fatalf("Seal: %v", err)
|
||||
}
|
||||
sealed.Ciphertext[len(sealed.Ciphertext)-1] ^= 0xFF
|
||||
|
||||
if _, err := svc.Open(context.Background(), "acme", sealed); !errors.Is(err, ErrDecryptFailed) {
|
||||
t.Fatalf("erwartet ErrDecryptFailed bei manipuliertem Ciphertext, erhalten: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package policyclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAllowed_TenantScopedRequest_TrueOnAllow(t *testing.T) {
|
||||
var gotBody map[string]string
|
||||
var gotToken string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotToken = r.Header.Get("X-Service-Token")
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||
t.Fatalf("body dekodieren: %v", err)
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]bool{"allowed": true})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(srv.URL, "svc-token-123")
|
||||
allowed, err := c.Allowed(context.Background(), "acme", "editor", "connector.write")
|
||||
if err != nil {
|
||||
t.Fatalf("Allowed: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Fatal("erwartet allowed=true")
|
||||
}
|
||||
if gotToken != "svc-token-123" {
|
||||
t.Fatalf("Service-Token nicht gesendet, erhalten: %q", gotToken)
|
||||
}
|
||||
if gotBody["tenant"] != "acme" {
|
||||
t.Fatalf("Tenant fehlt/falsch in Anfrage: %q", gotBody["tenant"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowed_DeniedResponse_False(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]bool{"allowed": false})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(srv.URL, "svc-token-123")
|
||||
allowed, err := c.Allowed(context.Background(), "acme", "viewer", "connector.write")
|
||||
if err != nil {
|
||||
t.Fatalf("Allowed: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Fatal("erwartet allowed=false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowed_ServiceFailure_DeniesInsteadOfSilentAllow(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(srv.URL, "svc-token-123")
|
||||
allowed, err := c.Allowed(context.Background(), "acme", "editor", "connector.write")
|
||||
if err == nil {
|
||||
t.Fatal("erwarteter Fehler bei Status 500 blieb aus")
|
||||
}
|
||||
if allowed {
|
||||
t.Fatal("Ausfall darf nie zu allowed=true fuehren")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowed_TransportFailure_DeniesInsteadOfSilentAllow(t *testing.T) {
|
||||
c := New("http://127.0.0.1:1", "svc-token-123")
|
||||
allowed, err := c.Allowed(context.Background(), "acme", "editor", "connector.write")
|
||||
if err == nil {
|
||||
t.Fatal("erwarteter Transport-Fehler blieb aus")
|
||||
}
|
||||
if allowed {
|
||||
t.Fatal("Ausfall darf nie zu allowed=true fuehren")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Package testcorpus erzeugt den QA-01-Referenzkorpus fuer Connect: je
|
||||
// angebundenem Drittsystemtyp (ERP, CRM, DATEV, Microsoft Graph) eine
|
||||
// Beispielantwort, deren Feldnamen und Struktur den real veroeffentlichten
|
||||
// Schnittstellenschemata der jeweiligen Systemklasse entsprechen, aber mit
|
||||
// vollstaendig erfundenen (nicht aus einem echten Kunden-/Altsystem
|
||||
// stammenden) Werten befuellt sind. Grundlage:
|
||||
// - ERP: generische REST-Belegstruktur, wie sie erpconnector als
|
||||
// Belegdatensatz liest/schreibt (internal/erpconnector).
|
||||
// - CRM: HubSpot CRM API v3 Contact-Objektform
|
||||
// (https://developers.hubspot.com/docs/api/crm/contacts) - "id" +
|
||||
// "properties"-Map, real dokumentiertes, weit verbreitetes CRM-Schema.
|
||||
// - DATEV: EXTF-Buchungsstapel-Zeile exakt im von datevexport erzeugten
|
||||
// Format (Format-Kategorie 21, siehe internal/datevexport).
|
||||
// - Microsoft Graph: v1.0-Antwortform fuer GET /users
|
||||
// (https://learn.microsoft.com/en-us/graph/api/user-list) inkl.
|
||||
// "@odata.context" und "value"-Array, real dokumentiertes Schema.
|
||||
package testcorpus
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Systems listet die vier in dieser Kachel geforderten Drittsystemtypen.
|
||||
var Systems = []string{"erp", "crm", "datev", "m365"}
|
||||
|
||||
// Generate schreibt fuer jeden Systemtyp genau eine Beispielantwort unter
|
||||
// outDir/<systemtyp>/. Vorhandene Dateien werden ueberschrieben, damit der
|
||||
// Korpus jederzeit reproduzierbar per Kommando neu erzeugt werden kann.
|
||||
func Generate(outDir string) error {
|
||||
writers := map[string]func(dir string) error{
|
||||
"erp": writeERP,
|
||||
"crm": writeCRM,
|
||||
"datev": writeDATEV,
|
||||
"m365": writeGraph,
|
||||
}
|
||||
for _, sys := range Systems {
|
||||
dir := filepath.Join(outDir, sys)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("testcorpus: verzeichnis %s: %w", dir, err)
|
||||
}
|
||||
if err := writers[sys](dir); err != nil {
|
||||
return fmt.Errorf("testcorpus: %s: %w", sys, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeJSON(path string, v any) error {
|
||||
data, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, append(data, '\n'), 0o644)
|
||||
}
|
||||
|
||||
// writeERP erzeugt eine Beispiel-Belegantwort, wie sie erpconnector.Client
|
||||
// von einem angebundenen ERP-System per generischem REST-Read erwartet
|
||||
// (Belegkopf + Positionen, Feldnamen deutsch wie im Fachbereich ueblich).
|
||||
func writeERP(dir string) error {
|
||||
beleg := map[string]any{
|
||||
"belegnummer": "RE-2026-004821",
|
||||
"belegdatum": "2026-03-14",
|
||||
"kunde": map[string]any{
|
||||
"debitorennummer": "10023",
|
||||
"name": "Musterhandel Nordlicht GmbH",
|
||||
"ustid": "DE999999999",
|
||||
},
|
||||
"positionen": []map[string]any{
|
||||
{"artikelnummer": "ART-1004", "bezeichnung": "Buerobedarf Sortiment A", "menge": 12, "einzelpreis": 4.90, "steuersatz": 19},
|
||||
{"artikelnummer": "ART-2071", "bezeichnung": "Wartungsvertrag Q1", "menge": 1, "einzelpreis": 249.00, "steuersatz": 19},
|
||||
},
|
||||
"gesamtbetrag_netto": 307.80,
|
||||
"gesamtbetrag_brutto": 366.28,
|
||||
"waehrung": "EUR",
|
||||
}
|
||||
return writeJSON(filepath.Join(dir, "beleg_response.json"), beleg)
|
||||
}
|
||||
|
||||
// writeCRM erzeugt eine Beispielantwort im HubSpot-CRM-v3-Contact-Schema
|
||||
// (real dokumentiertes Feldschema: id + properties-Map).
|
||||
func writeCRM(dir string) error {
|
||||
contact := map[string]any{
|
||||
"id": "51023",
|
||||
"properties": map[string]any{
|
||||
"firstname": "Anna",
|
||||
"lastname": "Testperson",
|
||||
"email": "anna.testperson@musterhandel-nordlicht.example",
|
||||
"company": "Musterhandel Nordlicht GmbH",
|
||||
"phone": "+49 431 5550100",
|
||||
"lifecyclestage": "customer",
|
||||
},
|
||||
"createdAt": "2025-11-02T09:14:00Z",
|
||||
"updatedAt": "2026-03-10T16:41:22Z",
|
||||
}
|
||||
return writeJSON(filepath.Join(dir, "contact_response.json"), contact)
|
||||
}
|
||||
|
||||
// writeDATEV erzeugt eine EXTF-Buchungsstapel-Beispielzeile exakt im
|
||||
// Format, das internal/datevexport produziert (Header + eine Datenzeile),
|
||||
// als Referenz fuer Rundtrip-/Format-Pruefungen kuenftiger Prüfgates.
|
||||
func writeDATEV(dir string) error {
|
||||
content := "EXTF;700;21;Buchungsstapel;9;20260314;;;;;;20260101;20261231;4;;;\r\n" +
|
||||
"Umsatz (ohne Soll/Haben-Kz);Soll/Haben-Kennzeichen;Konto;Gegenkonto (ohne BU-Schluessel);Belegdatum;Buchungstext;Belegfeld 1\r\n" +
|
||||
"307,80;S;10023;8400;14032026;Wartungsvertrag Q1 Musterhandel Nordlicht GmbH;RE-2026-004821\r\n"
|
||||
return os.WriteFile(filepath.Join(dir, "buchungsstapel.csv"), []byte(content), 0o644)
|
||||
}
|
||||
|
||||
// writeGraph erzeugt eine Beispielantwort im Microsoft-Graph-v1.0-Schema
|
||||
// fuer GET /users (real dokumentiertes Schema: "@odata.context" +
|
||||
// "value"-Array).
|
||||
func writeGraph(dir string) error {
|
||||
resp := map[string]any{
|
||||
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users",
|
||||
"value": []map[string]any{
|
||||
{
|
||||
"id": "9f4b3c1a-0000-4c11-9a2e-000000000001",
|
||||
"displayName": "Anna Testperson",
|
||||
"mail": "anna.testperson@musterhandel-nordlicht.example",
|
||||
"userPrincipalName": "anna.testperson@musterhandel-nordlicht.example",
|
||||
"jobTitle": "Sachbearbeitung Buchhaltung",
|
||||
},
|
||||
},
|
||||
}
|
||||
return writeJSON(filepath.Join(dir, "users_response.json"), resp)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package testcorpus
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestGenerate_AllSystemTypesCoveredAndParseable prueft QA-01 Prüfung 2:
|
||||
// der Testkorpus deckt alle vier geforderten Drittsystemtypen ab und jede
|
||||
// erzeugte Datei ist fuer ihren Systemtyp gueltig strukturiert.
|
||||
func TestGenerate_AllSystemTypesCoveredAndParseable(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := Generate(dir); err != nil {
|
||||
t.Fatalf("Generate: %v", err)
|
||||
}
|
||||
|
||||
for _, sys := range Systems {
|
||||
entries, err := os.ReadDir(filepath.Join(dir, sys))
|
||||
if err != nil {
|
||||
t.Fatalf("systemtyp %s: verzeichnis fehlt: %v", sys, err)
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
t.Fatalf("systemtyp %s: keine korpusdatei erzeugt", sys)
|
||||
}
|
||||
}
|
||||
|
||||
assertJSONObjectField(t, filepath.Join(dir, "erp", "beleg_response.json"), "belegnummer")
|
||||
assertJSONObjectField(t, filepath.Join(dir, "crm", "contact_response.json"), "properties")
|
||||
assertJSONObjectField(t, filepath.Join(dir, "m365", "users_response.json"), "@odata.context")
|
||||
assertDATEVFormat(t, filepath.Join(dir, "datev", "buchungsstapel.csv"))
|
||||
}
|
||||
|
||||
func assertJSONObjectField(t *testing.T, path, field string) {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: lesen: %v", path, err)
|
||||
}
|
||||
var obj map[string]any
|
||||
if err := json.Unmarshal(data, &obj); err != nil {
|
||||
t.Fatalf("%s: kein gueltiges JSON: %v", path, err)
|
||||
}
|
||||
if _, ok := obj[field]; !ok {
|
||||
t.Fatalf("%s: erwartetes Feld %q fehlt", path, field)
|
||||
}
|
||||
}
|
||||
|
||||
func assertDATEVFormat(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: lesen: %v", path, err)
|
||||
}
|
||||
lines := strings.Split(string(data), "\r\n")
|
||||
if len(lines) < 3 {
|
||||
t.Fatalf("%s: erwartet Header-Zeile + Feldkopf + mind. eine Datenzeile, hat %d Zeilen", path, len(lines))
|
||||
}
|
||||
if !strings.HasPrefix(lines[0], "EXTF;") {
|
||||
t.Fatalf("%s: erste Zeile keine gueltige EXTF-Kopfzeile: %q", path, lines[0])
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenerate_Idempotent prueft, dass der Korpus per Kommando jederzeit
|
||||
// reproduzierbar neu erzeugt werden kann (AC2: reproduzierbarer Bestand).
|
||||
func TestGenerate_Idempotent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := Generate(dir); err != nil {
|
||||
t.Fatalf("erster Lauf: %v", err)
|
||||
}
|
||||
first, err := os.ReadFile(filepath.Join(dir, "erp", "beleg_response.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("lesen nach erstem Lauf: %v", err)
|
||||
}
|
||||
if err := Generate(dir); err != nil {
|
||||
t.Fatalf("zweiter Lauf: %v", err)
|
||||
}
|
||||
second, err := os.ReadFile(filepath.Join(dir, "erp", "beleg_response.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("lesen nach zweitem Lauf: %v", err)
|
||||
}
|
||||
if string(first) != string(second) {
|
||||
t.Fatal("wiederholte Generate()-Laeufe liefern unterschiedliche Inhalte")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# Merge-Gate QA-01 Akzeptanzkriterium 4 (analog dms/scripts/check-security-
|
||||
# test-coverage.sh, FDN-07/dms-QA-01): sicherheitsrelevante Pakete (Tenant-
|
||||
# Scoping, Verschluesselung, Auth-nahe Zugriffspruefung gegen Core) muessen
|
||||
# mindestens eine Testdatei mit mindestens einer func Test... besitzen.
|
||||
# Liste per grep -rl "TenantSlug\|TenantID" internal --include=*.go sowie
|
||||
# manueller Pruefung auf Krypto-/Autorisierungs-Naehe ermittelt.
|
||||
set -euo pipefail
|
||||
|
||||
SECURITY_PACKAGES=(
|
||||
"internal/crypto"
|
||||
"internal/credential"
|
||||
"internal/auditclient"
|
||||
"internal/audittrail"
|
||||
"internal/policyclient"
|
||||
"internal/archiveclient"
|
||||
"internal/datevexport"
|
||||
"internal/datevtransfer"
|
||||
"internal/datevapi"
|
||||
"internal/dms2import"
|
||||
"internal/dms2importapi"
|
||||
"internal/fieldmapping"
|
||||
"internal/masterdatasync"
|
||||
"internal/syncjob"
|
||||
"internal/statusapi"
|
||||
)
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
fail=0
|
||||
for pkg in "${SECURITY_PACKAGES[@]}"; do
|
||||
if [ ! -d "$pkg" ]; then
|
||||
echo "FEHLER: sicherheitsrelevantes Paket fehlt: $pkg" >&2
|
||||
fail=1
|
||||
continue
|
||||
fi
|
||||
|
||||
test_files=$(find "$pkg" -maxdepth 1 -name '*_test.go')
|
||||
if [ -z "$test_files" ]; then
|
||||
echo "FEHLER: kein Test in sicherheitsrelevantem Paket: $pkg" >&2
|
||||
fail=1
|
||||
continue
|
||||
fi
|
||||
|
||||
has_test_func=0
|
||||
for f in $test_files; do
|
||||
if grep -q '^func Test' "$f"; then
|
||||
has_test_func=1
|
||||
fi
|
||||
done
|
||||
if [ "$has_test_func" -eq 0 ]; then
|
||||
echo "FEHLER: Testdatei(en) in $pkg enthalten keine func Test...: $test_files" >&2
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
echo "Merge-Gate FEHLGESCHLAGEN: sicherheitsrelevanter Code ohne Pflichttest." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Merge-Gate bestanden: alle sicherheitsrelevanten Pakete haben Tests."
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# Beweistest fuer das Merge-Gate selbst (analog dms/scripts/...): entfernt
|
||||
# testweise alle Tests eines sicherheitsrelevanten Pakets in einer Kopie des
|
||||
# Repos und bestaetigt, dass das Gate den Merge dann ablehnt (Exit-Code != 0)
|
||||
# - sowie dass es im Normalfall durchlaesst (Exit-Code 0).
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
WORKDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$WORKDIR"' EXIT
|
||||
|
||||
cp -r "$REPO_ROOT" "$WORKDIR/connect"
|
||||
|
||||
echo "== Fall 1: unveraendertes Repo -> Gate muss bestehen =="
|
||||
if ! bash "$WORKDIR/connect/scripts/check-security-test-coverage.sh"; then
|
||||
echo "FEHLER: Gate schlug im Normalfall fehl (falsch-positiv)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "== Fall 2: Tests von internal/crypto entfernt -> Gate muss ablehnen =="
|
||||
rm -f "$WORKDIR"/connect/internal/crypto/*_test.go
|
||||
if bash "$WORKDIR/connect/scripts/check-security-test-coverage.sh"; then
|
||||
echo "FEHLER: Gate liess Merge ohne Sicherheitstests durch (falsch-negativ)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Beweistest bestanden: Gate lehnt fehlende Sicherheitstests technisch ab."
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"createdAt": "2025-11-02T09:14:00Z",
|
||||
"id": "51023",
|
||||
"properties": {
|
||||
"company": "Musterhandel Nordlicht GmbH",
|
||||
"email": "anna.testperson@musterhandel-nordlicht.example",
|
||||
"firstname": "Anna",
|
||||
"lastname": "Testperson",
|
||||
"lifecyclestage": "customer",
|
||||
"phone": "+49 431 5550100"
|
||||
},
|
||||
"updatedAt": "2026-03-10T16:41:22Z"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
EXTF;700;21;Buchungsstapel;9;20260314;;;;;;20260101;20261231;4;;;
|
||||
Umsatz (ohne Soll/Haben-Kz);Soll/Haben-Kennzeichen;Konto;Gegenkonto (ohne BU-Schluessel);Belegdatum;Buchungstext;Belegfeld 1
|
||||
307,80;S;10023;8400;14032026;Wartungsvertrag Q1 Musterhandel Nordlicht GmbH;RE-2026-004821
|
||||
|
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"belegdatum": "2026-03-14",
|
||||
"belegnummer": "RE-2026-004821",
|
||||
"gesamtbetrag_brutto": 366.28,
|
||||
"gesamtbetrag_netto": 307.8,
|
||||
"kunde": {
|
||||
"debitorennummer": "10023",
|
||||
"name": "Musterhandel Nordlicht GmbH",
|
||||
"ustid": "DE999999999"
|
||||
},
|
||||
"positionen": [
|
||||
{
|
||||
"artikelnummer": "ART-1004",
|
||||
"bezeichnung": "Buerobedarf Sortiment A",
|
||||
"einzelpreis": 4.9,
|
||||
"menge": 12,
|
||||
"steuersatz": 19
|
||||
},
|
||||
{
|
||||
"artikelnummer": "ART-2071",
|
||||
"bezeichnung": "Wartungsvertrag Q1",
|
||||
"einzelpreis": 249,
|
||||
"menge": 1,
|
||||
"steuersatz": 19
|
||||
}
|
||||
],
|
||||
"waehrung": "EUR"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users",
|
||||
"value": [
|
||||
{
|
||||
"displayName": "Anna Testperson",
|
||||
"id": "9f4b3c1a-0000-4c11-9a2e-000000000001",
|
||||
"jobTitle": "Sachbearbeitung Buchhaltung",
|
||||
"mail": "anna.testperson@musterhandel-nordlicht.example",
|
||||
"userPrincipalName": "anna.testperson@musterhandel-nordlicht.example"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user