Neues Paket mail/internal/mailapi: drei v1-Endpunkte (Mail-Liste, Mail-Detail, Anhang-Download). Core API-01 (REST-Grundgerüst) und API-04 (OpenAPI-Beschreibung) haben im aktuellen Repository-Stand keinen abrufbaren Router — RegisterRoutes registriert die Endpunkte deshalb auf einem vom Aufrufer bereitgestellten *http.ServeMux mit dem dokumentierten Pfadschema /api/v1/mail/..., Core kann sich später dort einhängen, im Prüfprotokoll begründet (gleiche Situation wie ARC-06/Core TEN-01). tenant-Query-Parameter ist auf allen drei Endpunkten Pflicht (fehlender Kontext -> 400), keine eigene Login-/Session-Logik (IAM bleibt Core-Board-Sache). Anhang-Download nutzt storage.ObjectKey gegen den physisch getrennten Bucket des Mandanten (ARC-06) — ein Anhang mit identischer messageID in einem fremden Mandantenkontext ist strukturell nicht erreichbar. Neue Methode search.Client.GetByMessageID liefert das vollständige Suchdokument für Mail-Detail. openapi.yaml: vollständiger OpenAPI-3-Beitrag für alle drei Endpunkte inklusive Fehlerantworten. Als neue, gepinnte Abhängigkeit github.com/getkin/kin-openapi v0.135.0 (bewusst nicht @latest — hätte das Modul von go 1.24 auf go 1.25 gezwungen) für einen echten Standard-Validierungslauf gegen das Dokument sowie einen OpenAPI-Router, der jede implementierte Route real gegen das Dokument auflöst statt nur Pfad-Strings zu vergleichen. Alle vier Pflichtprüfungen mit echten Nachweisen: Zugriff ohne Tenant-Kontext auf allen drei Endpunkten abgelehnt, Vertragstests inkl. physischer Bucket-Trennung beim Anhang-Download, automatisiertes Code-Review bestätigt Abwesenheit IAM-naher Bezeichner, OpenAPI-Dokument validiert fehlerfrei gegen kin-openapi. go build/go vet/golangci-lint clean, go mod verify clean, gesamtes Mail-Modul regressionsfrei getestet.
98 lines
3.3 KiB
Go
98 lines
3.3 KiB
Go
package mailapi
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/getkin/kin-openapi/openapi3"
|
|
"github.com/getkin/kin-openapi/routers/gorillamux"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/mail/internal/search"
|
|
)
|
|
|
|
// TestOpenAPIDocument_ValidatesAgainstStandardTool ist die geforderte
|
|
// Pflichtprüfung 4 (INT-01): Validierungslauf des OpenAPI-Dokuments
|
|
// gegen ein Standardwerkzeug — github.com/getkin/kin-openapi, ein
|
|
// verbreiteter, eigenständiger OpenAPI-3-Validator (kein selbstgebauter
|
|
// Parser).
|
|
func TestOpenAPIDocument_ValidatesAgainstStandardTool(t *testing.T) {
|
|
loader := openapi3.NewLoader()
|
|
doc, err := loader.LoadFromFile("openapi.yaml")
|
|
if err != nil {
|
|
t.Fatalf("openapi.yaml laden: %v", err)
|
|
}
|
|
if err := doc.Validate(context.Background()); err != nil {
|
|
t.Fatalf("openapi.yaml ist gegen den Standardvalidator NICHT gültig: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestOpenAPIDocument_MatchesActualEndpoints ist die geforderte
|
|
// Pflichtprüfung (Akzeptanzkriterium 4: "... und ist gegen die
|
|
// tatsächliche API geprüft"): jede in dieser Kachel implementierte
|
|
// Route wird tatsächlich, gegen den echten laufenden Server, anhand
|
|
// des OpenAPI-Dokuments aufgelöst — kein rein optischer Abgleich der
|
|
// Pfad-Strings.
|
|
func TestOpenAPIDocument_MatchesActualEndpoints(t *testing.T) {
|
|
loader := openapi3.NewLoader()
|
|
doc, err := loader.LoadFromFile("openapi.yaml")
|
|
if err != nil {
|
|
t.Fatalf("openapi.yaml laden: %v", err)
|
|
}
|
|
if err := doc.Validate(context.Background()); err != nil {
|
|
t.Fatalf("openapi.yaml validieren: %v", err)
|
|
}
|
|
router, err := gorillamux.NewRouter(doc)
|
|
if err != nil {
|
|
t.Fatalf("router aus openapi.yaml bauen: %v", err)
|
|
}
|
|
|
|
ts, sc, _ := setupTestServer(t)
|
|
sc.put("mandant-a", search.Document{MessageID: "msg-1", Subject: "Test"})
|
|
|
|
cases := []struct {
|
|
method string
|
|
url string
|
|
}{
|
|
{http.MethodGet, "/api/v1/mail/messages?tenant=mandant-a"},
|
|
{http.MethodGet, "/api/v1/mail/messages/msg-1?tenant=mandant-a"},
|
|
{http.MethodGet, "/api/v1/mail/messages/msg-1/attachments/0?tenant=mandant-a"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.method+" "+c.url, func(t *testing.T) {
|
|
req, err := http.NewRequest(c.method, ts.URL+c.url, nil)
|
|
if err != nil {
|
|
t.Fatalf("request bauen: %v", err)
|
|
}
|
|
route, _, err := router.FindRoute(req)
|
|
if err != nil {
|
|
t.Fatalf("route für %s %s nicht im OpenAPI-Dokument gefunden: %v", c.method, c.url, err)
|
|
}
|
|
if route == nil {
|
|
t.Fatalf("keine route gefunden für %s %s", c.method, c.url)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCodeReview_NoIAMRelatedHandlers ist die geforderte Pflichtprüfung
|
|
// "Codereview bestätigt Abgrenzung zu Core-Board-Zuständigkeiten"
|
|
// (Akzeptanzkriterium 3) — automatisiert statt nur behauptet: kein
|
|
// Handler-/Routenname dieses Pakets enthält IAM-nahe Begriffe
|
|
// (Login/Session/Token/Tenant-Verwaltung).
|
|
func TestCodeReview_NoIAMRelatedHandlers(t *testing.T) {
|
|
content, err := os.ReadFile("mailapi.go")
|
|
if err != nil {
|
|
t.Fatalf("mailapi.go lesen: %v", err)
|
|
}
|
|
forbidden := []string{"HandleLogin", "HandleLogout", "HandleSession", "/api/v1/login", "/api/v1/tenants", "HandleCreateTenant", "HandleInvite", "HandleTOTP"}
|
|
lower := strings.ToLower(string(content))
|
|
for _, f := range forbidden {
|
|
if strings.Contains(lower, strings.ToLower(f)) {
|
|
t.Fatalf("mailapi.go enthält IAM-nahen bezeichner %q — gehört ins Core-Board, nicht in diese API", f)
|
|
}
|
|
}
|
|
}
|