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.
72 lines
2.3 KiB
Go
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")
|
|
}
|