CMP-08: ende-zu-ende-nachweis-ediscovery-adapter-reale-drittmodul-integration #327
No files matched your search
@@ -88,3 +88,128 @@ und nachgewiesen: eDiscovery-Export gegen den offiziellen Referenzdatensatz mit
|
||||
Testrollen (Erfolgsfall und erneut scheiternder Alleingang); Protokoll mit Zielwert/Messwert/
|
||||
Bewertung abgelegt, offene Restrisiken schriftlich benannt. Freigabe der Compliance-Funktionen aus
|
||||
Sicht dieses Gates erteilt — Gesamtabnahme bleibt QA-05 vorbehalten.
|
||||
|
||||
## Nachtrag CMP-08 (Stand 2026-10-16): Ende-zu-Ende-Nachweis des Adapters gegen echte DMS-/Mail-Testinstanzen
|
||||
|
||||
Schließt den oben in Abschnitt "Restrisiken" benannten Punkt **"eDiscovery-Adapter real nur
|
||||
fiktiv"**. Neue Produktivkomponente `archive/internal/ediscoveryhttp` (analog
|
||||
`kekclient`/`rbacclient`): `DMSAdapter` und `MailAdapter` implementieren
|
||||
`ediscovery.ModuleAdapter` über echtes HTTP gegen die realen REST-Endpunkte von DMS
|
||||
(`dms/internal/searchapi` `GET /search`, `dms/internal/documentapi` `GET
|
||||
/documents/{id}/content`) bzw. Mail (`mail/internal/mailapi` `GET /api/v1/mail/messages`,
|
||||
`GET /api/v1/mail/messages/{id}`, mit Modul-Token `aud=mail`).
|
||||
|
||||
### Echte Testinstanzen (Host 192.168.1.204)
|
||||
|
||||
- **DMS**: echter `dms/internal/searchwiring` + neu ergänzter `dms/internal/documentapi` auf
|
||||
einem frisch migrierten Schema der bestehenden Dev-Datenbank `dms_dev`
|
||||
(`NEXARCH_DMS_APP_TENANT_DSN` aus `/etc/nexarch-dev/dms-dev.env`), echtes Manticore
|
||||
(`TEST_MANTICORE_ADDR`), 4 synthetische PDF-Belege über den echten Upload-/Worker-Pfad
|
||||
(`dms/internal/upload`, `dms/internal/workerruntime`, `dms/internal/testcorpus`) hochgeladen
|
||||
und über die echte Volltextsuche auffindbar gemacht — keine Fixture-Doubles.
|
||||
- **Mail**: echter `mail/internal/mailapi.Server`, mit echtem `mail/internal/search.Client`
|
||||
(Manticore-Tabelle `mail_documents`, eigener Mandant `cmp08-ediscovery-mail`) und echtem
|
||||
`mail/internal/storage.Service`; 4 synthetische Nachrichten über `search.Client.Index`
|
||||
eingetragen. Mail hat noch keinen eigenen `cmd/app`-Produktivserver (siehe
|
||||
`mail/internal/mailapi`-Paketkommentar) — Modul-Token-Verifikation läuft über die ECHTE
|
||||
Verifikationslogik `mail/internal/moduleauth.Verifier`, mit einem lokal erzeugten
|
||||
Test-Schlüsselpaar statt eines laufenden Core-Kontodienstes (`NewTestVerifierAndIssuer`,
|
||||
dieselbe Konvention wie `mail/internal/mailapi/e2e_test.go`) — siehe Restrisiko unten.
|
||||
- Beide Instanzen liefen als Wegwerf-Prüfwerkzeuge (`dms/cmd/cmp08-ediscovery-fixture`,
|
||||
`mail/cmd/cmp08-ediscovery-fixture`), nach dem Testlauf wieder entfernt — nicht Teil dieses
|
||||
Commits, nur die Produktivkomponente (`ediscoveryhttp`) und der Nachweistest bleiben.
|
||||
- Alle Testdaten synthetisch (Suchbegriff `CmpAchtEdiscoveryFund`, Absendername
|
||||
`pruefperson@cmp08.example.test`), keine echten Kunden-/Mandantendaten.
|
||||
|
||||
### Prüfung 1 (Akzeptanzkriterium 1): Export gegen echte, über RET-05 registrierte Instanzen
|
||||
|
||||
Vor dem eigentlichen Such-/Exportlauf registriert der Test beide Instanzen real über das
|
||||
bestehende RET-05-Interface (`archive/internal/moduleadapter.Register`) gegen die echte
|
||||
Postgres-Testdatenbank (`TEST_TENANT_DSN`): `module_name=dms/object_type=dms_document` mit
|
||||
Rückruf-Adresse der echten laufenden DMS-Instanz, `module_name=mail/object_type=mail_message`
|
||||
mit Rückruf-Adresse der echten laufenden Mail-Instanz.
|
||||
|
||||
| Prüfgröße | Zielwert | Messwert | Bewertung |
|
||||
|---|---|---|---|
|
||||
| Registrierungen in `module_registrations` nach Register() | 2 (DMS + Mail) | 2 | bestanden |
|
||||
| Export erfolgt gegen die registrierten, laufenden Instanzen (nicht Referenzdatensatz) | ja | ja — `ediscoveryhttp.DMSAdapter`/`MailAdapter` rufen die echten Endpunkte auf | bestanden |
|
||||
|
||||
### Prüfung 2 (Akzeptanzkriterium 2): Treffer aus beiden Modulen, prüfsummengesichert
|
||||
|
||||
Test `TestEndToEnd_CMP08_RealDMSAndMailInstances`
|
||||
(`archive/internal/ediscoveryhttp/e2e_test.go`): `ediscovery.Search()` über beide echten
|
||||
Adapter, `ediscovery.Export()` baut das ZIP, der Test entpackt es erneut und vergleicht die
|
||||
SHA-256-Prüfsumme jeder Datei gegen den `manifest.json`-Eintrag.
|
||||
|
||||
| Prüfgröße | Zielwert | Messwert | Bewertung |
|
||||
|---|---|---|---|
|
||||
| Treffer mit `module_type=dms_document` im Suchergebnis | ≥ 1 | 4 | bestanden |
|
||||
| Treffer mit `module_type=mail_message` im Suchergebnis | ≥ 1 | 4 | bestanden |
|
||||
| Prüfsumme nach Entpacken stimmt für jede Datei (beide Modultypen) | 100 % Übereinstimmung | 100 % (8/8 Dateien) | bestanden |
|
||||
|
||||
### Prüfung 3 (Pflichtprüfung 2): Export-Prüfsummen bei wiederholtem Testlauf identisch
|
||||
|
||||
Zwei unabhängige `Search()+Export()`-Durchläufe INNERHALB desselben Testlaufs (frischer
|
||||
HTTP-Roundtrip gegen die echten Instanzen, nicht wiederverwendete Treffer), Vergleich über
|
||||
`modultyp+referenz` statt Listenposition (Reihenfolge der Suchtreffer ist zwischen Läufen nicht
|
||||
garantiert stabil). Zusätzlich der komplette Test zweimal als VOLLSTÄNDIG unabhängiger
|
||||
`go test`-Prozessaufruf gegen dieselben laufenden Instanzen wiederholt.
|
||||
|
||||
| Prüfgröße | Zielwert | Messwert | Bewertung |
|
||||
|---|---|---|---|
|
||||
| Prüfsummen Lauf 1 vs. Lauf 2 (innerhalb des Tests, je Treffer) | identisch | identisch (8/8) | bestanden |
|
||||
| Testergebnis über 2 unabhängige Prozessaufrufe hinweg | PASS, PASS | PASS, PASS | bestanden |
|
||||
|
||||
### Build-/Testergebnis (192.168.1.204)
|
||||
|
||||
```
|
||||
go build ./... -> clean
|
||||
go vet ./... -> clean
|
||||
go test ./internal/ediscoveryhttp/... -v -> PASS (2x unabhaengiger Prozessaufruf)
|
||||
go test ./... -p 1 -> alle Archive-Pakete bestanden (2x wiederholt)
|
||||
```
|
||||
|
||||
Ein einzelner, nicht reproduzierbarer Fehlschlag in `archive/internal/reconcile`
|
||||
(`TestListDBStorageKeys_ReadsRealFileRevisions`, "erwartet 1 Eintrag, habe 9") trat einmalig
|
||||
während eines `go test ./... -p 1`-Laufs auf. Isoliert ausgeführt sowie in je zwei weiteren
|
||||
vollständigen Suite-Läufen (mit UND ohne die CMP-08-Änderungen) bestand dieser Test
|
||||
durchgehend — kein durch CMP-08 verursachter Regressionsbefund, sondern dieselbe Art
|
||||
Testisolations-Lücke gegen die geteilte `TEST_TENANT_DSN`-Datenbank, wie sie für das DMS-Modul
|
||||
bereits dokumentiert ist. Nicht in dieser Kachel behoben (unverändertes, fremdes Paket).
|
||||
|
||||
### Restrisiken (neu, durch CMP-08 gefunden)
|
||||
|
||||
- **Mail hat noch keinen RET-05-Rückruf-EMPFÄNGER**: DMS registriert sich bereits produktiv
|
||||
selbst gegen RET-05 inkl. echtem Rückruf-Empfänger (`dms/cmd/app` `setupRetention`,
|
||||
DOC-16, `POST /retention/destroy-callback`). Für Mail existiert dieses Gegenstück noch nicht
|
||||
(nur der RET-13-Sperrabfrage-Client `mail/internal/retentionclient`) — die in diesem Test
|
||||
registrierte Rückruf-Adresse der Mail-Instanz ist real erreichbar, aber die Instanz
|
||||
verarbeitet eingehende Vernichtungs-Rückrufe fachlich noch nicht. Empfehlung: eigenes
|
||||
Folgeticket, Mail-seitige Rückruf-Verarbeitung analog DOC-16 ergänzen, sobald Mail einen
|
||||
eigenen `cmd/app` bekommt.
|
||||
- **Mail-Modul-Token stammt aus einem Test-Schlüsselpaar, nicht vom echten Core-Kontodienst**:
|
||||
weil Mail noch keinen produktiven Server-Prozess hat, an den ein echter Core-JWKS-Abruf
|
||||
gebunden werden könnte (anders als bei DMS, dessen `cmd/app` dafür bereits verdrahtet ist).
|
||||
Die Verifikationslogik selbst (`moduleauth.Verifier`) ist unverändert die Produktivfunktion.
|
||||
Ein Nachweis mit einem vom echten Core ausgestellten Token (wie in
|
||||
`mail/internal/mailapi/e2e_test.go` `TestEndToEnd_RealCoreToken`) ist erst möglich, sobald ein
|
||||
Core-Kontodienst auf demselben Testhost erreichbar ist — außerhalb des Zuschnitts dieser
|
||||
Kachel (keine neue eDiscovery-Funktionalität, kein Core-Deployment).
|
||||
- **RET-05-Tenant-Verifikation (Core TEN-01) nicht Teil dieses Nachweises**: die Registrierung
|
||||
erfolgte über den direkten Aufruf von `moduleadapter.Register` (die reale
|
||||
Persistenzfunktion), nicht über den HTTP-Endpunkt `archive/cmd/moduleadapter-api` mit
|
||||
`tenantregistryclient.VerifyTenant` gegen einen echten Core-TEN-01-Dienst — letzterer war auf
|
||||
diesem Testhost nicht aufgesetzt (separates Infrastrukturthema, keine eDiscovery-Logik). Die
|
||||
eigentliche RET-05-Speicherlogik (Register/ListRegistrations, Idempotenz) ist bereits durch
|
||||
RET-05/RET-09 real geprüft und wird hier nicht erneut bewiesen.
|
||||
- golangci-lint weiterhin nicht auf Host 204 installiert (siehe QA-03 oben, unverändert).
|
||||
|
||||
### Gesamtergebnis CMP-08
|
||||
|
||||
**Bestanden.** Alle drei Akzeptanzkriterien und beide Pflichtprüfungen real gegen echte,
|
||||
laufende DMS- und Mail-Testinstanzen (keine Referenzdatensatz-Fixtures) nachgewiesen:
|
||||
RET-05-Registrierung beider Instanzen, Suchtreffer aus beiden Modulen im Export,
|
||||
prüfsummengesicherte Übereinstimmung nach Entpacken, Reproduzierbarkeit über zwei
|
||||
In-Test-Läufe UND zwei unabhängige Prozessaufrufe hinweg. Neue, bisher offene Restrisiken
|
||||
schriftlich benannt (Mail-seitiger Rückruf-Empfänger fehlt, Core-Token-Ausstellung für Mail
|
||||
noch nicht end-to-end, RET-05-Tenant-Verifikation nicht mitgeprüft).
|
||||
@@ -0,0 +1,190 @@
|
||||
// CMP-08 Ende-zu-Ende-Nachweis: ediscovery.Search/Export real gegen
|
||||
// laufende, echte DMS-/Mail-Testinstanzen (nicht gegen Referenzdatensatz-
|
||||
// Fixtures wie QA-03), ueber den ediscoveryhttp-Adapter, Instanzen vorher
|
||||
// real ueber RET-05 (moduleadapter.Register) registriert. Laeuft nur, wenn
|
||||
// die CMP08_*-Umgebungsvariablen gesetzt sind (siehe
|
||||
// scripts/cmp08-ediscovery-e2e.sh) - gleiche Konvention wie
|
||||
// mail/internal/mailapi/e2e_test.go.
|
||||
package ediscoveryhttp
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/ediscovery"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/moduleadapter"
|
||||
)
|
||||
|
||||
type cmp08Env struct {
|
||||
dmsBaseURL, dmsCaller, dmsTenant string
|
||||
mailBaseURL, mailToken, mailTenant string
|
||||
term, tenantDSN string
|
||||
}
|
||||
|
||||
func loadCMP08Env(t *testing.T) cmp08Env {
|
||||
t.Helper()
|
||||
e := cmp08Env{
|
||||
dmsBaseURL: os.Getenv("CMP08_DMS_BASE_URL"),
|
||||
dmsCaller: os.Getenv("CMP08_DMS_CALLER_ID"),
|
||||
dmsTenant: os.Getenv("CMP08_DMS_TENANT_SLUG"),
|
||||
mailBaseURL: os.Getenv("CMP08_MAIL_BASE_URL"),
|
||||
mailToken: os.Getenv("CMP08_MAIL_TOKEN"),
|
||||
mailTenant: os.Getenv("CMP08_MAIL_TENANT_SLUG"),
|
||||
term: os.Getenv("CMP08_SEARCH_TERM"),
|
||||
tenantDSN: os.Getenv("TEST_TENANT_DSN"),
|
||||
}
|
||||
if e.dmsBaseURL == "" || e.mailBaseURL == "" || e.tenantDSN == "" || e.term == "" {
|
||||
t.Skip("CMP08_*/TEST_TENANT_DSN nicht gesetzt, Ende-zu-Ende-Nachweis uebersprungen")
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func requireRegistrationPool(t *testing.T, dsn string) *pgxpool.Pool {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
pool, err := pgxpool.New(ctx, dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("pool: %v", err)
|
||||
}
|
||||
t.Cleanup(pool.Close)
|
||||
if _, err := pool.Exec(ctx, `
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
CREATE TABLE IF NOT EXISTS module_registrations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_slug TEXT NOT NULL, module_name TEXT NOT NULL,
|
||||
object_type TEXT NOT NULL, retention_class TEXT NOT NULL, callback_url TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (module_name, object_type)
|
||||
);
|
||||
`); err != nil {
|
||||
t.Fatalf("schema: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _, _ = pool.Exec(context.Background(), `TRUNCATE module_registrations`) })
|
||||
return pool
|
||||
}
|
||||
|
||||
// manifestChecksums liest ein ediscovery.Export-ZIP ein, prueft die
|
||||
// manifest.json-Pruefsumme jeder Datei gegen den tatsaechlich entpackten
|
||||
// Inhalt (Akzeptanzkriterium 2) und liefert die Pruefsummen, indiziert
|
||||
// ueber modultyp+referenz statt Listenposition (Suchtreffer-Reihenfolge
|
||||
// ist zwischen zwei Laeufen nicht garantiert stabil).
|
||||
func manifestChecksums(t *testing.T, zipBytes []byte) map[string]string {
|
||||
t.Helper()
|
||||
zr, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
|
||||
if err != nil {
|
||||
t.Fatalf("zip lesen: %v", err)
|
||||
}
|
||||
var manifest []ediscovery.ManifestEntry
|
||||
files := map[string][]byte{}
|
||||
for _, f := range zr.File {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
t.Fatalf("zip-eintrag %q oeffnen: %v", f.Name, err)
|
||||
}
|
||||
content, err := io.ReadAll(rc)
|
||||
_ = rc.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("zip-eintrag %q lesen: %v", f.Name, err)
|
||||
}
|
||||
if f.Name == "manifest.json" {
|
||||
if err := json.Unmarshal(content, &manifest); err != nil {
|
||||
t.Fatalf("manifest dekodieren: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
files[f.Name] = content
|
||||
}
|
||||
out := map[string]string{}
|
||||
for _, m := range manifest {
|
||||
path := m.ModuleType + "/" + m.Filename
|
||||
content, ok := files[path]
|
||||
if !ok {
|
||||
t.Fatalf("manifest verweist auf fehlende datei %q", path)
|
||||
}
|
||||
sum := sha256.Sum256(content)
|
||||
if hex.EncodeToString(sum[:]) != m.ChecksumSHA256 {
|
||||
t.Fatalf("pruefsumme stimmt nicht fuer %q", path)
|
||||
}
|
||||
out[m.ModuleType+"/"+m.Reference] = m.ChecksumSHA256
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func runSearchAndExport(t *testing.T, ctx context.Context, adapters []ediscovery.ModuleAdapter, byType map[string]ediscovery.ModuleAdapter, term string) map[string]string {
|
||||
t.Helper()
|
||||
hits, errs := ediscovery.Search(ctx, nil, "cmp08-ediscovery", "cmp08-pruefperson", term, adapters)
|
||||
if len(errs) != 0 {
|
||||
t.Fatalf("suche gegen reale instanzen fehlgeschlagen: %v", errs)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, h := range hits {
|
||||
seen[h.ModuleType] = true
|
||||
}
|
||||
if !seen[ModuleTypeDMSDocument] {
|
||||
t.Fatalf("keine treffer aus dms_document in: %+v", hits)
|
||||
}
|
||||
if !seen[ModuleTypeMailMessage] {
|
||||
t.Fatalf("keine treffer aus mail_message in: %+v", hits)
|
||||
}
|
||||
zipBytes, err := ediscovery.Export(ctx, nil, "cmp08-ediscovery", "cmp08-pruefperson", byType, hits)
|
||||
if err != nil {
|
||||
t.Fatalf("export fehlgeschlagen: %v", err)
|
||||
}
|
||||
return manifestChecksums(t, zipBytes)
|
||||
}
|
||||
|
||||
// TestEndToEnd_CMP08_RealDMSAndMailInstances deckt alle drei Akzeptanz-
|
||||
// kriterien ab: Export gegen echte, ueber RET-05 registrierte DMS-/
|
||||
// Mail-Testinstanzen (1), Treffer aus beiden Modulen pruefsummengesichert
|
||||
// im Export (2), Pruefsummen bei wiederholtem Testlauf identisch (3).
|
||||
func TestEndToEnd_CMP08_RealDMSAndMailInstances(t *testing.T) {
|
||||
e := loadCMP08Env(t)
|
||||
ctx := context.Background()
|
||||
|
||||
pool := requireRegistrationPool(t, e.tenantDSN)
|
||||
if _, err := moduleadapter.Register(ctx, pool, e.dmsTenant, "dms", ModuleTypeDMSDocument, "klasse-ediscovery-test", e.dmsBaseURL+"/retention/destroy-callback"); err != nil {
|
||||
t.Fatalf("dms via ret-05 registrieren: %v", err)
|
||||
}
|
||||
if _, err := moduleadapter.Register(ctx, pool, e.mailTenant, "mail", ModuleTypeMailMessage, "klasse-ediscovery-test", e.mailBaseURL+"/api/v1/mail/messages"); err != nil {
|
||||
t.Fatalf("mail via ret-05 registrieren: %v", err)
|
||||
}
|
||||
regs, err := moduleadapter.ListRegistrations(ctx, pool)
|
||||
if err != nil {
|
||||
t.Fatalf("registrierungen lesen: %v", err)
|
||||
}
|
||||
if len(regs) != 2 {
|
||||
t.Fatalf("erwartete 2 ret-05-registrierungen (dms+mail), habe %d: %+v", len(regs), regs)
|
||||
}
|
||||
|
||||
dmsAdapter := NewDMSAdapter(e.dmsBaseURL, e.dmsCaller, nil)
|
||||
mailAdapter := NewMailAdapter(e.mailBaseURL, e.mailToken, nil)
|
||||
adapters := []ediscovery.ModuleAdapter{dmsAdapter, mailAdapter}
|
||||
byType := map[string]ediscovery.ModuleAdapter{ModuleTypeDMSDocument: dmsAdapter, ModuleTypeMailMessage: mailAdapter}
|
||||
|
||||
run1 := runSearchAndExport(t, ctx, adapters, byType, e.term)
|
||||
run2 := runSearchAndExport(t, ctx, adapters, byType, e.term)
|
||||
|
||||
if len(run1) == 0 {
|
||||
t.Fatal("erster lauf lieferte keine pruefsummen")
|
||||
}
|
||||
if len(run1) != len(run2) {
|
||||
t.Fatalf("unterschiedliche trefferzahl zwischen den laeufen: %d vs %d", len(run1), len(run2))
|
||||
}
|
||||
for key, sum1 := range run1 {
|
||||
sum2, ok := run2[key]
|
||||
if !ok {
|
||||
t.Fatalf("treffer %q im zweiten lauf nicht mehr vorhanden", key)
|
||||
}
|
||||
if sum1 != sum2 {
|
||||
t.Fatalf("pruefsumme fuer %q unterscheidet sich zwischen den laeufen: %s vs %s", key, sum1, sum2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// Package ediscoveryhttp bindet archive/internal/ediscovery.ModuleAdapter
|
||||
// ueber HTTP an die echten DMS-/Mail-REST-Endpunkte (gleiches Muster wie
|
||||
// kekclient/rbacclient) - der in CMP-03/QA-03 offen benannte Restrisiko-Punkt
|
||||
// ("ediscovery.ModuleAdapter wird in Produktion ueber HTTP gebunden, aber nie
|
||||
// real getestet"), geschlossen durch CMP-08.
|
||||
package ediscoveryhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/ediscovery"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpclient"
|
||||
)
|
||||
|
||||
const ModuleTypeDMSDocument = "dms_document"
|
||||
const ModuleTypeMailMessage = "mail_message"
|
||||
|
||||
// DMSAdapter ruft DMS GET /search und GET /documents/{id}/content auf
|
||||
// (dms/internal/searchapi bzw. dms/internal/documentapi).
|
||||
type DMSAdapter struct {
|
||||
baseURL string
|
||||
caller string
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
func NewDMSAdapter(baseURL, caller string, client *http.Client) *DMSAdapter {
|
||||
if client == nil {
|
||||
client = httpclient.Default()
|
||||
}
|
||||
return &DMSAdapter{baseURL: baseURL, caller: caller, http: client}
|
||||
}
|
||||
|
||||
func (a *DMSAdapter) ModuleType() string { return ModuleTypeDMSDocument }
|
||||
|
||||
type dmsHit struct {
|
||||
DocumentID string `json:"document_id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
type dmsSearchResponse struct {
|
||||
Hits []dmsHit `json:"hits"`
|
||||
}
|
||||
|
||||
func (a *DMSAdapter) Search(ctx context.Context, criteria string) ([]ediscovery.ObjectRef, error) {
|
||||
q := url.Values{"q": {criteria}, "caller": {a.caller}, "limit": {"100"}}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+"/search?"+q.Encode(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ediscoveryhttp(dms): anfrage bauen: %w", err)
|
||||
}
|
||||
resp, err := a.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ediscoveryhttp(dms): suche fehlgeschlagen: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("ediscoveryhttp(dms): suche status %d", resp.StatusCode)
|
||||
}
|
||||
var body dmsSearchResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
return nil, fmt.Errorf("ediscoveryhttp(dms): antwort dekodieren: %w", err)
|
||||
}
|
||||
refs := make([]ediscovery.ObjectRef, 0, len(body.Hits))
|
||||
for _, h := range body.Hits {
|
||||
refs = append(refs, ediscovery.ObjectRef{ModuleType: ModuleTypeDMSDocument, Reference: h.DocumentID, Filename: h.Title})
|
||||
}
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
func (a *DMSAdapter) FetchContent(ctx context.Context, reference string) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+"/documents/"+url.PathEscape(reference)+"/content", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ediscoveryhttp(dms): anfrage bauen: %w", err)
|
||||
}
|
||||
resp, err := a.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ediscoveryhttp(dms): inhalt abrufen: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("ediscoveryhttp(dms): inhalt status %d", resp.StatusCode)
|
||||
}
|
||||
return io.ReadAll(io.LimitReader(resp.Body, maxContentBytes))
|
||||
}
|
||||
|
||||
// MailAdapter ruft die Mail-API v1 auf (mail/internal/mailapi), mit einem
|
||||
// vom aufrufenden Kontext ausgestellten Modul-Token (aud=mail).
|
||||
type MailAdapter struct {
|
||||
baseURL string
|
||||
token string
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
func NewMailAdapter(baseURL, moduleToken string, client *http.Client) *MailAdapter {
|
||||
if client == nil {
|
||||
client = httpclient.Default()
|
||||
}
|
||||
return &MailAdapter{baseURL: baseURL, token: moduleToken, http: client}
|
||||
}
|
||||
|
||||
func (a *MailAdapter) ModuleType() string { return ModuleTypeMailMessage }
|
||||
|
||||
type mailMessageListItem struct {
|
||||
MessageID string `json:"messageId"`
|
||||
Subject string `json:"subject"`
|
||||
}
|
||||
|
||||
type mailListResponse struct {
|
||||
Messages []mailMessageListItem `json:"messages"`
|
||||
}
|
||||
|
||||
func (a *MailAdapter) authed(ctx context.Context, path string) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+path, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ediscoveryhttp(mail): anfrage bauen: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+a.token)
|
||||
resp, err := a.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ediscoveryhttp(mail): anfrage senden: %w", err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (a *MailAdapter) Search(ctx context.Context, criteria string) ([]ediscovery.ObjectRef, error) {
|
||||
resp, err := a.authed(ctx, "/api/v1/mail/messages?q="+url.QueryEscape(criteria))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("ediscoveryhttp(mail): suche status %d", resp.StatusCode)
|
||||
}
|
||||
var body mailListResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
return nil, fmt.Errorf("ediscoveryhttp(mail): antwort dekodieren: %w", err)
|
||||
}
|
||||
refs := make([]ediscovery.ObjectRef, 0, len(body.Messages))
|
||||
for _, m := range body.Messages {
|
||||
refs = append(refs, ediscovery.ObjectRef{ModuleType: ModuleTypeMailMessage, Reference: m.MessageID, Filename: m.MessageID + ".txt"})
|
||||
}
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
// FetchContent liefert die rohen Antwort-Bytes von GET
|
||||
// /messages/{id} (Betreff+Text) als Exportinhalt - Mail-Nachrichten haben
|
||||
// kein "Original-Binaerformat" wie DMS-Dokumente, der strukturierte
|
||||
// JSON-Datensatz IST der Originalinhalt dieses Treffers.
|
||||
func (a *MailAdapter) FetchContent(ctx context.Context, reference string) ([]byte, error) {
|
||||
resp, err := a.authed(ctx, "/api/v1/mail/messages/"+url.PathEscape(reference))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("ediscoveryhttp(mail): inhalt status %d", resp.StatusCode)
|
||||
}
|
||||
return io.ReadAll(io.LimitReader(resp.Body, maxContentBytes))
|
||||
}
|
||||
|
||||
const maxContentBytes = 50 << 20
|
||||
Reference in new issue
Block a user