package internal_test import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "testing" "time" "mgit.msbls.de/m/KanzlAI-mGMT/internal/auth" "mgit.msbls.de/m/KanzlAI-mGMT/internal/config" "mgit.msbls.de/m/KanzlAI-mGMT/internal/db" "mgit.msbls.de/m/KanzlAI-mGMT/internal/router" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" ) // testServer sets up the full router with a real DB connection. // Requires DATABASE_URL and SUPABASE_JWT_SECRET env vars. func testServer(t *testing.T) (http.Handler, func()) { t.Helper() dbURL := os.Getenv("DATABASE_URL") if dbURL == "" { t.Skip("DATABASE_URL not set, skipping integration test") } jwtSecret := os.Getenv("SUPABASE_JWT_SECRET") if jwtSecret == "" { jwtSecret = "test-jwt-secret-for-integration-tests" os.Setenv("SUPABASE_JWT_SECRET", jwtSecret) } database, err := db.Connect(dbURL) if err != nil { t.Fatalf("connecting to database: %v", err) } cfg := &config.Config{ Port: "0", DatabaseURL: dbURL, SupabaseJWTSecret: jwtSecret, } authMW := auth.NewMiddleware(jwtSecret, database) handler := router.New(database, authMW, cfg, nil) return handler, func() { database.Close() } } // createTestJWT creates a JWT for the given user ID, signed with the test secret. func createTestJWT(t *testing.T, userID uuid.UUID) string { t.Helper() secret := os.Getenv("SUPABASE_JWT_SECRET") if secret == "" { secret = "test-jwt-secret-for-integration-tests" } claims := jwt.MapClaims{ "sub": userID.String(), "aud": "authenticated", "exp": time.Now().Add(1 * time.Hour).Unix(), "iat": time.Now().Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signed, err := token.SignedString([]byte(secret)) if err != nil { t.Fatalf("signing JWT: %v", err) } return signed } // setupTestTenant creates a temporary tenant and user_tenant for testing. // Returns tenantID and userID. Cleans up on test completion. func setupTestTenant(t *testing.T, handler http.Handler) (tenantID, userID uuid.UUID) { t.Helper() dbURL := os.Getenv("DATABASE_URL") database, err := db.Connect(dbURL) if err != nil { t.Fatalf("connecting to database for setup: %v", err) } defer database.Close() tenantID = uuid.New() userID = uuid.New() _, err = database.Exec(`INSERT INTO tenants (id, name, slug) VALUES ($1, $2, $3)`, tenantID, "Integration Test Kanzlei "+tenantID.String()[:8], "test-"+tenantID.String()[:8]) if err != nil { t.Fatalf("creating test tenant: %v", err) } // Create a user in auth.users so JWT auth works _, err = database.Exec(`INSERT INTO auth.users (id, instance_id, role, aud, email, encrypted_password, email_confirmed_at, created_at, updated_at, confirmation_token, recovery_token) VALUES ($1, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', $2, '', NOW(), NOW(), NOW(), '', '')`, userID, fmt.Sprintf("test-%s@kanzlai.test", userID.String()[:8])) if err != nil { t.Fatalf("creating test user: %v", err) } _, err = database.Exec(`INSERT INTO user_tenants (user_id, tenant_id, role) VALUES ($1, $2, 'owner')`, userID, tenantID) if err != nil { t.Fatalf("creating user_tenant: %v", err) } t.Cleanup(func() { cleanupDB, _ := db.Connect(dbURL) if cleanupDB != nil { cleanupDB.Exec(`DELETE FROM case_events WHERE tenant_id = $1`, tenantID) cleanupDB.Exec(`DELETE FROM deadlines WHERE tenant_id = $1`, tenantID) cleanupDB.Exec(`DELETE FROM appointments WHERE tenant_id = $1`, tenantID) cleanupDB.Exec(`DELETE FROM parties WHERE tenant_id = $1`, tenantID) cleanupDB.Exec(`DELETE FROM documents WHERE tenant_id = $1`, tenantID) cleanupDB.Exec(`DELETE FROM cases WHERE tenant_id = $1`, tenantID) cleanupDB.Exec(`DELETE FROM user_tenants WHERE tenant_id = $1`, tenantID) cleanupDB.Exec(`DELETE FROM tenants WHERE id = $1`, tenantID) cleanupDB.Exec(`DELETE FROM auth.users WHERE id = $1`, userID) cleanupDB.Close() } }) return tenantID, userID } func TestHealthEndpoint(t *testing.T) { handler, cleanup := testServer(t) defer cleanup() req := httptest.NewRequest("GET", "/health", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var resp map[string]string json.Unmarshal(w.Body.Bytes(), &resp) if resp["status"] != "ok" { t.Fatalf("expected status ok, got %v", resp) } } func TestCriticalPath_CreateCase_AddDeadline_Dashboard(t *testing.T) { handler, cleanup := testServer(t) defer cleanup() _, userID := setupTestTenant(t, handler) token := createTestJWT(t, userID) // Step 1: Create a case caseBody := `{"case_number":"TEST/001","title":"Integration Test — Patentverletzung","case_type":"patent","court":"UPC München"}` req := httptest.NewRequest("POST", "/api/cases", bytes.NewBufferString(caseBody)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("create case: expected 201, got %d: %s", w.Code, w.Body.String()) } var createdCase map[string]interface{} json.Unmarshal(w.Body.Bytes(), &createdCase) caseID, ok := createdCase["id"].(string) if !ok || caseID == "" { t.Fatalf("create case: missing ID in response: %v", createdCase) } t.Logf("Created case: %s", caseID) // Step 2: List cases — verify our case is there req = httptest.NewRequest("GET", "/api/cases", nil) req.Header.Set("Authorization", "Bearer "+token) w = httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("list cases: expected 200, got %d: %s", w.Code, w.Body.String()) } var caseList map[string]interface{} json.Unmarshal(w.Body.Bytes(), &caseList) total := caseList["total"].(float64) if total < 1 { t.Fatalf("list cases: expected at least 1 case, got %.0f", total) } // Step 3: Add a deadline to the case dueDate := time.Now().AddDate(0, 0, 14).Format("2006-01-02") warnDate := time.Now().AddDate(0, 0, 10).Format("2006-01-02") deadlineBody := fmt.Sprintf(`{"title":"Klageerwiderung einreichen","due_date":"%s","warning_date":"%s","source":"manual"}`, dueDate, warnDate) req = httptest.NewRequest("POST", "/api/cases/"+caseID+"/deadlines", bytes.NewBufferString(deadlineBody)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) w = httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("create deadline: expected 201, got %d: %s", w.Code, w.Body.String()) } var createdDeadline map[string]interface{} json.Unmarshal(w.Body.Bytes(), &createdDeadline) deadlineID, ok := createdDeadline["id"].(string) if !ok || deadlineID == "" { t.Fatalf("create deadline: missing ID in response: %v", createdDeadline) } t.Logf("Created deadline: %s", deadlineID) // Step 4: Verify deadline appears in case deadlines req = httptest.NewRequest("GET", "/api/cases/"+caseID+"/deadlines", nil) req.Header.Set("Authorization", "Bearer "+token) w = httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("list deadlines: expected 200, got %d: %s", w.Code, w.Body.String()) } var deadlines []interface{} json.Unmarshal(w.Body.Bytes(), &deadlines) if len(deadlines) < 1 { t.Fatalf("list deadlines: expected at least 1, got %d", len(deadlines)) } // Step 5: Fetch dashboard — should include our case and deadline req = httptest.NewRequest("GET", "/api/dashboard", nil) req.Header.Set("Authorization", "Bearer "+token) w = httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("dashboard: expected 200, got %d: %s", w.Code, w.Body.String()) } var dashboard map[string]interface{} json.Unmarshal(w.Body.Bytes(), &dashboard) // Verify dashboard has expected sections for _, key := range []string{"deadline_summary", "case_summary", "upcoming_deadlines"} { if _, exists := dashboard[key]; !exists { t.Errorf("dashboard: missing key %q in response", key) } } // Verify case_summary shows at least 1 active case if cs, ok := dashboard["case_summary"].(map[string]interface{}); ok { if active, ok := cs["active"].(float64); ok && active < 1 { t.Errorf("dashboard: expected at least 1 active case, got %.0f", active) } } t.Logf("Full critical path passed: auth -> create case -> add deadline -> dashboard") } func TestUnauthenticatedAccess(t *testing.T) { handler, cleanup := testServer(t) defer cleanup() // Accessing API without token should return 401 req := httptest.NewRequest("GET", "/api/cases", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Fatalf("expected 401, got %d: %s", w.Code, w.Body.String()) } }