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.
73 lines
2.1 KiB
Go
73 lines
2.1 KiB
Go
package smtp
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
// TestTenantScoping_ConcurrentServersNeverMixMessages ist die
|
|
// geforderte Pflichtprüfung (ING-10, Akzeptanzkriterium 2): Tenant-
|
|
// Scoping für den SMTP-Ingestion-Pfad. Zwei vollständig unabhängige
|
|
// Server-Instanzen (Mandant A/B), GLEICHZEITIG mit vielen Nachrichten
|
|
// bedient — jede Instanz bekommt nur ihren eigenen Sink injiziert.
|
|
// Eine Vermischung würde sich hier als falscher Nachrichteninhalt beim
|
|
// jeweils anderen Sink zeigen.
|
|
func TestTenantScoping_ConcurrentServersNeverMixMessages(t *testing.T) {
|
|
sinkA := &fakeSink{}
|
|
sinkB := &fakeSink{}
|
|
addrA, stopA := startTestServer(t, sinkA, defaultMaxMessageBytes)
|
|
defer stopA()
|
|
addrB, stopB := startTestServer(t, sinkB, defaultMaxMessageBytes)
|
|
defer stopB()
|
|
|
|
const perTenant = 20
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < perTenant; i++ {
|
|
wg.Add(2)
|
|
go func(n int) {
|
|
defer wg.Done()
|
|
sendTenantMessage(t, addrA, "Mandant-A")
|
|
}(i)
|
|
go func(n int) {
|
|
defer wg.Done()
|
|
sendTenantMessage(t, addrB, "Mandant-B")
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
if sinkA.count() != perTenant {
|
|
t.Fatalf("mandant A: erwartete %d nachrichten, habe %d", perTenant, sinkA.count())
|
|
}
|
|
if sinkB.count() != perTenant {
|
|
t.Fatalf("mandant B: erwartete %d nachrichten, habe %d", perTenant, sinkB.count())
|
|
}
|
|
for _, m := range sinkA.accepted {
|
|
if !strings.Contains(string(m.raw), "Mandant-A") || strings.Contains(string(m.raw), "Mandant-B") {
|
|
t.Fatalf("mandant A hat fremden/vermischten inhalt bekommen: %q", m.raw)
|
|
}
|
|
}
|
|
for _, m := range sinkB.accepted {
|
|
if !strings.Contains(string(m.raw), "Mandant-B") || strings.Contains(string(m.raw), "Mandant-A") {
|
|
t.Fatalf("mandant B hat fremden/vermischten inhalt bekommen: %q", m.raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
func sendTenantMessage(t *testing.T, addr, marker string) {
|
|
t.Helper()
|
|
c := dial(t, addr)
|
|
defer c.close()
|
|
c.send(t, "EHLO client.example.com")
|
|
for {
|
|
line := c.readLine(t)
|
|
if strings.HasPrefix(line, "250 ") {
|
|
break
|
|
}
|
|
}
|
|
c.send(t, "MAIL FROM:<a@example.com>")
|
|
c.send(t, "RCPT TO:<b@example.com>")
|
|
c.send(t, "DATA")
|
|
c.send(t, "Subject: "+marker+"\r\n\r\nInhalt von "+marker+"\r\n.")
|
|
}
|