package pop3 import ( "bufio" "context" "net" "strings" "testing" "time" ) // tenantScopedMailboxStore ist ein In-Memory-Postfachspeicher EINES // Mandanten — bewusst eine eigene, unabhängige Instanz je Mandant statt // eines gemeinsamen Stores mit tenant-Parameter, um die // Pflichtprüfung realistisch nachzustellen: der POP3-Server bekommt // beim Aufbau NUR den Store des eigenen Mandanten injiziert und hat // strukturell keinen Zugriff auf den eines anderen (Akzeptanzkriterium // 2, ING-10). func newTenantScopedStore(tenant string) *fakeMailboxStore { return &fakeMailboxStore{messages: map[string]map[int]string{ "alice": {1: "Geheime Nachricht von Mandant " + tenant}, }} } // TestTenantScoping_IsolatedStoresNeverLeakAcrossServers ist die // geforderte Pflichtprüfung (ING-10, Akzeptanzkriterium 2): Tenant- // Scoping für den POP3-Ingestion-Pfad. Zwei vollständig unabhängige // Server-Instanzen (Mandant A/B) mit IDENTISCHEM Benutzernamen "alice" // und IDENTISCHEM Passwort, aber unterschiedlichem Postfachinhalt — // der Klartext-Realfall, in dem ein Bug am ehesten eine Vermischung // zeigen würde. func TestTenantScoping_IsolatedStoresNeverLeakAcrossServers(t *testing.T) { authA := fakeAuthenticator{users: map[string]string{"alice": "geheim123"}} authB := fakeAuthenticator{users: map[string]string{"alice": "geheim123"}} storeA := newTenantScopedStore("A") storeB := newTenantScopedStore("B") addrA, stopA := startPOP3Server(t, NewServer(authA, storeA)) defer stopA() addrB, stopB := startPOP3Server(t, NewServer(authB, storeB)) defer stopB() contentFromA := retrieveFirstMessage(t, addrA, "alice", "geheim123") contentFromB := retrieveFirstMessage(t, addrB, "alice", "geheim123") if !strings.Contains(contentFromA, "Mandant A") { t.Fatalf("mandant A hat nicht seine eigene nachricht bekommen: %q", contentFromA) } if !strings.Contains(contentFromB, "Mandant B") { t.Fatalf("mandant B hat nicht seine eigene nachricht bekommen: %q", contentFromB) } if strings.Contains(contentFromA, "Mandant B") || strings.Contains(contentFromB, "Mandant A") { t.Fatalf("mandantenvermischung: A=%q B=%q", contentFromA, contentFromB) } } func startPOP3Server(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 retrieveFirstMessage(t *testing.T, addr, username, password string) string { t.Helper() conn, err := net.DialTimeout("tcp", addr, 2*time.Second) if err != nil { t.Fatalf("dial: %v", err) } defer func() { _ = conn.Close() }() reader := bufio.NewReader(conn) _, _ = reader.ReadString('\n') _, _ = conn.Write([]byte("USER " + username + "\r\n")) _, _ = reader.ReadString('\n') _, _ = conn.Write([]byte("PASS " + password + "\r\n")) resp, _ := reader.ReadString('\n') if !strings.HasPrefix(resp, "+OK") { t.Fatalf("anmeldung fehlgeschlagen: %q", resp) } _, _ = conn.Write([]byte("RETR 1\r\n")) status, _ := reader.ReadString('\n') if !strings.HasPrefix(status, "+OK") { t.Fatalf("RETR fehlgeschlagen: %q", status) } var lines []string for { line, _ := reader.ReadString('\n') line = strings.TrimRight(line, "\r\n") if line == "." { break } lines = append(lines, line) } _, _ = conn.Write([]byte("QUIT\r\n")) _, _ = reader.ReadString('\n') return strings.Join(lines, "\n") }