Neues Paket mail/internal/healthcheck: Checker sammelt benannte CheckFunc-Prüfungen, liefert Gesamt- und Einzelstatus je Komponente (ok/degraded, nie ein generischer Fehler). Fehlertexte einzelner Prüfungen fließen nie in die HTTP-Antwort — nur Name+Status je Komponente. Vier konkrete Prüfungen (checks.go) gegen die real vorhandenen Ticket-Abhängigkeiten: DatabaseCheck (pgxpool.Ping), ObjectStorageCheck (HeadBucket gegen den ARC-06-Bucket), SearchIndexCheck (reale Manticore-Anfrage), JobQueueCheck (COUNT gegen mail_index_jobs, SRC-02/indexworker — COUNT statt Zeilenzugriff, damit eine leere aber erreichbare Queue nicht fälschlich als Ausfall gilt). RegisterRoutes registriert GET /api/v1/mail/health ohne Authentifizierung auf einem vom Aufrufer bereitgestellten *http.ServeMux, gleiches Pfadschema wie mailapi (INT-01) — Core API-01 hat weiterhin keinen abrufbaren Router. Alle drei Pflichtprüfungen mit echten Nachweisen: simulierter Ausfall einer von vier Abhängigkeiten korrekt als degraded abgebildet, eine Prüfung mit absichtlich eingebetteter Verbindungszeichenfolge inkl. Passwort im Fehlertext taucht nirgends in der HTTP-Antwort auf, echter httptest-HTTP-Server-Integrationstest gegen den Endpunkt. Alle vier konkreten Prüfungen zusätzlich real gegen laufende Postgres-, MinIO- und Manticore-Instanzen verifiziert (inkl. echter ARC-06-Provisionierung). go build/go vet/golangci-lint clean, gesamtes Mail-Modul regressionsfrei getestet.
150 lines
4.9 KiB
Go
150 lines
4.9 KiB
Go
// Integrationstests (INT-07): echte Postgres-, MinIO- und
|
|
// Manticore-Instanzen, gleiche Umgebungsvariablen-Konvention wie
|
|
// mail/internal/storage (TEST_S3_...) und mail/internal/folderstate
|
|
// (TEST_TENANT_DSN).
|
|
package healthcheck
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/mail/internal/search"
|
|
"gitea.perlbach24.de/scripte/nexarch/mail/internal/storage"
|
|
)
|
|
|
|
// TestDatabaseCheck_RealPostgres ist Teil der geforderten
|
|
// Pflichtprüfung "je Komponente getrennt" (Akzeptanzkriterium 1) —
|
|
// gegen eine echte, laufende Postgres-Instanz.
|
|
func TestDatabaseCheck_RealPostgres(t *testing.T) {
|
|
dsn := os.Getenv("TEST_TENANT_DSN")
|
|
if dsn == "" {
|
|
t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest übersprungen")
|
|
}
|
|
pool, err := pgxpool.New(context.Background(), dsn)
|
|
if err != nil {
|
|
t.Fatalf("pool: %v", err)
|
|
}
|
|
defer pool.Close()
|
|
|
|
check := DatabaseCheck(pool)
|
|
if err := check(context.Background()); err != nil {
|
|
t.Fatalf("DatabaseCheck gegen echte instanz fehlgeschlagen: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestJobQueueCheck_RealPostgres prüft die Jobqueue-Erreichbarkeit
|
|
// gegen eine echte Instanz — inklusive Schema-Anlage, damit der Test
|
|
// unabhängig davon läuft, ob indexworker bereits initialisiert wurde.
|
|
func TestJobQueueCheck_RealPostgres(t *testing.T) {
|
|
dsn := os.Getenv("TEST_TENANT_DSN")
|
|
if dsn == "" {
|
|
t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest übersprungen")
|
|
}
|
|
pool, err := pgxpool.New(context.Background(), dsn)
|
|
if err != nil {
|
|
t.Fatalf("pool: %v", err)
|
|
}
|
|
defer pool.Close()
|
|
if _, err := pool.Exec(context.Background(), `
|
|
CREATE TABLE IF NOT EXISTS mail_index_jobs (
|
|
id SERIAL PRIMARY KEY,
|
|
job_type TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending'
|
|
)
|
|
`); err != nil {
|
|
t.Fatalf("mail_index_jobs anlegen: %v", err)
|
|
}
|
|
|
|
check := JobQueueCheck(pool)
|
|
if err := check(context.Background()); err != nil {
|
|
t.Fatalf("JobQueueCheck gegen echte instanz fehlgeschlagen: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestObjectStorageCheck_RealMinIO prüft die Objektspeicher-
|
|
// Erreichbarkeit gegen eine echte MinIO-Instanz (echtes, per ARC-06
|
|
// provisioniertes Bucket).
|
|
func TestObjectStorageCheck_RealMinIO(t *testing.T) {
|
|
endpoint := os.Getenv("TEST_S3_ENDPOINT")
|
|
if endpoint == "" {
|
|
t.Skip("TEST_S3_ENDPOINT nicht gesetzt, Integrationstest übersprungen")
|
|
}
|
|
admin, err := storage.NewS3AdminClient(context.Background(), "us-east-1", endpoint, os.Getenv("TEST_S3_ACCESS_KEY"), os.Getenv("TEST_S3_SECRET_KEY"), true)
|
|
if err != nil {
|
|
t.Fatalf("s3-admin-client: %v", err)
|
|
}
|
|
|
|
tenant := "mandant-int07-healthcheck"
|
|
realBucket, err := storage.ProvisionTenant(context.Background(), mustRegistryPool(t), admin, tenant, "INT-07 Healthcheck", "postgresql://healthcheck")
|
|
if err != nil {
|
|
t.Fatalf("ProvisionTenant: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
ctx := context.Background()
|
|
out, err := admin.ListObjectsV2(ctx, &s3.ListObjectsV2Input{Bucket: aws.String(realBucket)})
|
|
if err == nil {
|
|
for _, obj := range out.Contents {
|
|
_, _ = admin.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: aws.String(realBucket), Key: obj.Key})
|
|
}
|
|
}
|
|
_, _ = admin.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: aws.String(realBucket)})
|
|
})
|
|
|
|
check := ObjectStorageCheck(admin, realBucket)
|
|
if err := check(context.Background()); err != nil {
|
|
t.Fatalf("ObjectStorageCheck gegen echtes bucket fehlgeschlagen: %v", err)
|
|
}
|
|
}
|
|
|
|
func mustRegistryPool(t *testing.T) *pgxpool.Pool {
|
|
t.Helper()
|
|
dsn := os.Getenv("TEST_TENANT_DSN")
|
|
if dsn == "" {
|
|
t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest übersprungen")
|
|
}
|
|
pool, err := pgxpool.New(context.Background(), dsn)
|
|
if err != nil {
|
|
t.Fatalf("pool: %v", err)
|
|
}
|
|
t.Cleanup(pool.Close)
|
|
if _, err := pool.Exec(context.Background(), `
|
|
CREATE TABLE IF NOT EXISTS tenants (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
slug TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
db_dsn TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
)
|
|
`); err != nil {
|
|
t.Fatalf("tenants-tabelle anlegen: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_, _ = pool.Exec(context.Background(), `DELETE FROM tenants WHERE slug = 'mandant-int07-healthcheck'`)
|
|
})
|
|
return pool
|
|
}
|
|
|
|
// TestSearchIndexCheck_RealManticore prüft die Suchindex-Erreichbarkeit
|
|
// gegen eine echte Manticore-Instanz.
|
|
func TestSearchIndexCheck_RealManticore(t *testing.T) {
|
|
baseURL := os.Getenv("TEST_MANTICORE_URL")
|
|
if baseURL == "" {
|
|
t.Skip("TEST_MANTICORE_URL nicht gesetzt, Integrationstest übersprungen")
|
|
}
|
|
client := search.NewClient(baseURL)
|
|
if err := client.EnsureSchema(context.Background()); err != nil {
|
|
t.Fatalf("schema sicherstellen: %v", err)
|
|
}
|
|
|
|
check := SearchIndexCheck(client)
|
|
if err := check(context.Background()); err != nil {
|
|
t.Fatalf("SearchIndexCheck gegen echte instanz fehlgeschlagen: %v", err)
|
|
}
|
|
}
|