Files
nexarch/mail/internal/imap/tenant_scoping_test.go
T
sysops 12c9037121 feat(mail): ING-10 Ingestion-Testsuite — Tenant-Scoping-Tests, mimeparse-Lücke geschlossen
Kein neues Produktionspaket, Audit- und Test-Kachel über die fünf
Ingestion-Module (IMAP, POP3, SMTP, MIME, Folder-State). Zwei konkrete
Lücken geschlossen:

Neuer tenant_scoping_test.go in allen fünf Paketen: je zwei simulierte
Mandanten mit ABSICHTLICH identischen Schlüsseln (Benutzername,
Postfachname) — der Realfall, in dem ein fehlendes Scoping-Prädikat am
ehesten eine echte Vermischung zeigen würde, statt trivial durch
unterschiedliche Schlüssel zu bestehen. IMAP/POP3: zwei unabhängige
Serverinstanzen mit je eigenem Store. SMTP: zwei Serverinstanzen,
gleichzeitig mit vielen Nachrichten bedient. mimeparse: paralleles
Parsen vieler "Mandanten"-Nachrichten (das Paket hat keinen
Datenbankzugriff — Tenant-Scoping bedeutet hier: kein geteilter
veränderlicher Zustand). folderstate: echte Postgres-Instanz,
NextUID/Rebuild für Mandant A dürfen Mandant Bs Zustand nachweislich
nicht verändern.

mimeparse.ParseTolerant (IMP-02) war zu 0% Zeilenabdeckung vollständig
ungetestet — genau der aus known-issues-archivmail.md #4 bekannte
Fehler (kritische Ingestion-Logik ohne Tests). Neue tolerant_test.go:
ein fehlerhafter Teil reißt die übrigen nicht mit, Gesamtgrößenlimit
über alle Teile hinweg, strukturell kaputte Multipart-Hülle liefert
weiterhin einen echten Fehler, Nicht-Multipart-Pfad. Abdeckung
mimeparse 44,0% -> 76,7%.

Testabdeckungsbericht für alle fünf Module dokumentiert, CI-Lauf auf
frischem Checkout ohne externe Live-Postfächer verifiziert grün.
Pflichtprüfung 3 (Stichprobenreview durch zweite Person) ist durch
eine einzelne Sitzung strukturell nicht erfüllbar und bleibt offen —
im Prüfprotokoll dokumentiert, Nutzer-Review ausstehend.

go build/go vet/golangci-lint clean, gesamtes Mail-Modul (~29 Pakete)
regressionsfrei getestet.
2026-09-01 09:35:45 +02:00

72 lines
2.3 KiB
Go

package imap
import (
"context"
"net"
"strings"
"testing"
)
// TestTenantScoping_IsolatedStoresNeverLeakAcrossServers ist die
// geforderte Pflichtprüfung (ING-10, Akzeptanzkriterium 2): Tenant-
// Scoping für den IMAP-Ingestion-Pfad. Zwei vollständig unabhängige
// Server-Instanzen (Mandant A/B) mit identischem Benutzernamen/Passwort
// und identischem Postfachnamen "INBOX", aber unterschiedlichem Inhalt
// (als Flag codiert, damit ein FETCH ihn sichtbar macht) — Bug würde
// sich hier als Vermischung der Flags zeigen.
func TestTenantScoping_IsolatedStoresNeverLeakAcrossServers(t *testing.T) {
auth := fakeAuthenticator{users: map[string]string{"alice": "geheim123"}}
storeA := fakeMailboxStore{mailboxes: map[string][]Message{
"INBOX": {{SequenceNumber: 1, UID: 1, Flags: []string{"Mandant-A-Marker"}}},
}}
storeB := fakeMailboxStore{mailboxes: map[string][]Message{
"INBOX": {{SequenceNumber: 1, UID: 1, Flags: []string{"Mandant-B-Marker"}}},
}}
addrA, stopA := startIMAPServer(t, NewServer(auth, storeA))
defer stopA()
addrB, stopB := startIMAPServer(t, NewServer(auth, storeB))
defer stopB()
fetchA := fetchInboxFlags(t, addrA)
fetchB := fetchInboxFlags(t, addrB)
if !strings.Contains(fetchA, "Mandant-A-Marker") {
t.Fatalf("mandant A hat nicht seine eigenen daten bekommen: %q", fetchA)
}
if !strings.Contains(fetchB, "Mandant-B-Marker") {
t.Fatalf("mandant B hat nicht seine eigenen daten bekommen: %q", fetchB)
}
if strings.Contains(fetchA, "Mandant-B-Marker") || strings.Contains(fetchB, "Mandant-A-Marker") {
t.Fatalf("mandantenvermischung: A=%q B=%q", fetchA, fetchB)
}
}
func startIMAPServer(t *testing.T, srv *Server) (addr string, stop func()) {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listener: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
_ = srv.Serve(ctx, listener)
close(done)
}()
return listener.Addr().String(), func() {
cancel()
<-done
}
}
func fetchInboxFlags(t *testing.T, addr string) string {
t.Helper()
c := dial(t, addr)
defer c.close()
c.sendTagged(t, "LOGIN alice geheim123")
c.sendTagged(t, "SELECT INBOX")
_, lines := c.sendTagged(t, "FETCH 1 (FLAGS)")
return strings.Join(lines, "\n")
}