test: comprehensive E2E and API test suite for full KanzlAI stack
Backend (Go): - Expanded integration_test.go: health, auth middleware (expired/invalid/wrong-secret JWT), tenant CRUD, case CRUD (create/list/get/update/delete + filters + validation), deadline CRUD (create/list/update/complete/delete), appointment CRUD, dashboard (verifies all sections), deadline calculator (valid/invalid/unknown type), proceeding types & rules, document endpoints, AI extraction (no-key path), and full critical path E2E (auth -> case -> deadline -> appointment -> dashboard -> complete) - New handler unit tests: case (10), appointment (11), dashboard (1), calculate (5), document (10), AI (4) — all testing validation, auth guards, and error paths without DB - Total: ~80 backend tests (unit + integration) Frontend (TypeScript/Vitest): - Installed vitest 2.x, @testing-library/react, @testing-library/jest-dom, jsdom 24, msw - vitest.config.ts with jsdom env, esbuild JSX automatic, path aliases - API client tests (13): URL construction, no double /api/, auth header, tenant header, POST/PUT/PATCH/DELETE methods, error handling, 204 responses - DeadlineTrafficLights tests (5): renders cards, correct counts, zero state, onFilter callback - CaseOverviewGrid tests (4): renders categories, counts, header, zero state - LoginPage tests (8): form rendering, mode toggle, password login, redirect, error display, magic link, registration link - Total: 30 frontend tests Makefile: test-frontend target now runs vitest instead of placeholder echo.
This commit is contained in:
74
backend/internal/handlers/ai_handler_test.go
Normal file
74
backend/internal/handlers/ai_handler_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAIExtractDeadlines_EmptyInput(t *testing.T) {
|
||||
h := &AIHandler{}
|
||||
|
||||
body := `{"text":""}`
|
||||
r := httptest.NewRequest("POST", "/api/ai/extract-deadlines", bytes.NewBufferString(body))
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.ExtractDeadlines(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
var resp map[string]string
|
||||
json.NewDecoder(w.Body).Decode(&resp)
|
||||
if resp["error"] != "provide either a PDF file or text" {
|
||||
t.Errorf("unexpected error: %s", resp["error"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIExtractDeadlines_InvalidJSON(t *testing.T) {
|
||||
h := &AIHandler{}
|
||||
|
||||
r := httptest.NewRequest("POST", "/api/ai/extract-deadlines", bytes.NewBufferString(`{broken`))
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.ExtractDeadlines(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAISummarizeCase_MissingCaseID(t *testing.T) {
|
||||
h := &AIHandler{}
|
||||
|
||||
body := `{"case_id":""}`
|
||||
r := httptest.NewRequest("POST", "/api/ai/summarize-case", bytes.NewBufferString(body))
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.SummarizeCase(w, r)
|
||||
|
||||
// Without auth context, the resolveTenant will fail first
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAISummarizeCase_InvalidJSON(t *testing.T) {
|
||||
h := &AIHandler{}
|
||||
|
||||
r := httptest.NewRequest("POST", "/api/ai/summarize-case", bytes.NewBufferString(`not-json`))
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.SummarizeCase(w, r)
|
||||
|
||||
// Without auth context, the resolveTenant will fail first
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
196
backend/internal/handlers/appointment_handler_test.go
Normal file
196
backend/internal/handlers/appointment_handler_test.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"mgit.msbls.de/m/KanzlAI-mGMT/internal/auth"
|
||||
)
|
||||
|
||||
func TestAppointmentCreate_NoTenant(t *testing.T) {
|
||||
h := &AppointmentHandler{}
|
||||
r := httptest.NewRequest("POST", "/api/appointments", bytes.NewBufferString(`{}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Create(w, r)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppointmentCreate_MissingTitle(t *testing.T) {
|
||||
h := &AppointmentHandler{}
|
||||
body := `{"start_at":"2026-04-01T10:00:00Z"}`
|
||||
r := httptest.NewRequest("POST", "/api/appointments", bytes.NewBufferString(body))
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Create(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
var resp map[string]string
|
||||
json.NewDecoder(w.Body).Decode(&resp)
|
||||
if resp["error"] != "title is required" {
|
||||
t.Errorf("unexpected error: %s", resp["error"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppointmentCreate_MissingStartAt(t *testing.T) {
|
||||
h := &AppointmentHandler{}
|
||||
body := `{"title":"Test Appointment"}`
|
||||
r := httptest.NewRequest("POST", "/api/appointments", bytes.NewBufferString(body))
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Create(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
var resp map[string]string
|
||||
json.NewDecoder(w.Body).Decode(&resp)
|
||||
if resp["error"] != "start_at is required" {
|
||||
t.Errorf("unexpected error: %s", resp["error"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppointmentCreate_InvalidJSON(t *testing.T) {
|
||||
h := &AppointmentHandler{}
|
||||
r := httptest.NewRequest("POST", "/api/appointments", bytes.NewBufferString(`{broken`))
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Create(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppointmentList_NoTenant(t *testing.T) {
|
||||
h := &AppointmentHandler{}
|
||||
r := httptest.NewRequest("GET", "/api/appointments", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.List(w, r)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppointmentUpdate_NoTenant(t *testing.T) {
|
||||
h := &AppointmentHandler{}
|
||||
r := httptest.NewRequest("PUT", "/api/appointments/"+uuid.New().String(), bytes.NewBufferString(`{}`))
|
||||
r.SetPathValue("id", uuid.New().String())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Update(w, r)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppointmentUpdate_InvalidID(t *testing.T) {
|
||||
h := &AppointmentHandler{}
|
||||
r := httptest.NewRequest("PUT", "/api/appointments/not-uuid", bytes.NewBufferString(`{}`))
|
||||
r.SetPathValue("id", "not-uuid")
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Update(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppointmentDelete_NoTenant(t *testing.T) {
|
||||
h := &AppointmentHandler{}
|
||||
r := httptest.NewRequest("DELETE", "/api/appointments/"+uuid.New().String(), nil)
|
||||
r.SetPathValue("id", uuid.New().String())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Delete(w, r)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppointmentDelete_InvalidID(t *testing.T) {
|
||||
h := &AppointmentHandler{}
|
||||
r := httptest.NewRequest("DELETE", "/api/appointments/bad", nil)
|
||||
r.SetPathValue("id", "bad")
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Delete(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppointmentList_InvalidCaseID(t *testing.T) {
|
||||
h := &AppointmentHandler{}
|
||||
r := httptest.NewRequest("GET", "/api/appointments?case_id=bad", nil)
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.List(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppointmentList_InvalidStartFrom(t *testing.T) {
|
||||
h := &AppointmentHandler{}
|
||||
r := httptest.NewRequest("GET", "/api/appointments?start_from=not-a-date", nil)
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.List(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
83
backend/internal/handlers/calculate_handler_test.go
Normal file
83
backend/internal/handlers/calculate_handler_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCalculate_MissingFields(t *testing.T) {
|
||||
h := &CalculateHandlers{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty body",
|
||||
body: `{}`,
|
||||
want: "proceeding_type and trigger_event_date are required",
|
||||
},
|
||||
{
|
||||
name: "missing trigger_event_date",
|
||||
body: `{"proceeding_type":"INF"}`,
|
||||
want: "proceeding_type and trigger_event_date are required",
|
||||
},
|
||||
{
|
||||
name: "missing proceeding_type",
|
||||
body: `{"trigger_event_date":"2026-06-01"}`,
|
||||
want: "proceeding_type and trigger_event_date are required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := httptest.NewRequest("POST", "/api/deadlines/calculate", bytes.NewBufferString(tt.body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Calculate(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
var resp map[string]string
|
||||
json.NewDecoder(w.Body).Decode(&resp)
|
||||
if resp["error"] != tt.want {
|
||||
t.Errorf("expected error %q, got %q", tt.want, resp["error"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculate_InvalidDateFormat(t *testing.T) {
|
||||
h := &CalculateHandlers{}
|
||||
body := `{"proceeding_type":"INF","trigger_event_date":"01-06-2026"}`
|
||||
r := httptest.NewRequest("POST", "/api/deadlines/calculate", bytes.NewBufferString(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Calculate(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
var resp map[string]string
|
||||
json.NewDecoder(w.Body).Decode(&resp)
|
||||
if resp["error"] != "invalid trigger_event_date format, expected YYYY-MM-DD" {
|
||||
t.Errorf("unexpected error: %s", resp["error"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculate_InvalidJSON(t *testing.T) {
|
||||
h := &CalculateHandlers{}
|
||||
r := httptest.NewRequest("POST", "/api/deadlines/calculate", bytes.NewBufferString(`not-json`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Calculate(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
177
backend/internal/handlers/case_handler_test.go
Normal file
177
backend/internal/handlers/case_handler_test.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"mgit.msbls.de/m/KanzlAI-mGMT/internal/auth"
|
||||
)
|
||||
|
||||
func TestCaseCreate_NoAuth(t *testing.T) {
|
||||
h := &CaseHandler{}
|
||||
r := httptest.NewRequest("POST", "/api/cases", bytes.NewBufferString(`{}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Create(w, r)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected 403, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseCreate_MissingFields(t *testing.T) {
|
||||
h := &CaseHandler{}
|
||||
body := `{"case_number":"","title":""}`
|
||||
r := httptest.NewRequest("POST", "/api/cases", bytes.NewBufferString(body))
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Create(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
var resp map[string]string
|
||||
json.NewDecoder(w.Body).Decode(&resp)
|
||||
if resp["error"] != "case_number and title are required" {
|
||||
t.Errorf("unexpected error: %s", resp["error"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseCreate_InvalidJSON(t *testing.T) {
|
||||
h := &CaseHandler{}
|
||||
r := httptest.NewRequest("POST", "/api/cases", bytes.NewBufferString(`not-json`))
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Create(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseGet_InvalidID(t *testing.T) {
|
||||
h := &CaseHandler{}
|
||||
r := httptest.NewRequest("GET", "/api/cases/not-a-uuid", nil)
|
||||
r.SetPathValue("id", "not-a-uuid")
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Get(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseGet_NoTenant(t *testing.T) {
|
||||
h := &CaseHandler{}
|
||||
r := httptest.NewRequest("GET", "/api/cases/"+uuid.New().String(), nil)
|
||||
r.SetPathValue("id", uuid.New().String())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Get(w, r)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected 403, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseList_NoTenant(t *testing.T) {
|
||||
h := &CaseHandler{}
|
||||
r := httptest.NewRequest("GET", "/api/cases", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.List(w, r)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected 403, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseUpdate_InvalidID(t *testing.T) {
|
||||
h := &CaseHandler{}
|
||||
body := `{"title":"Updated"}`
|
||||
r := httptest.NewRequest("PUT", "/api/cases/bad-id", bytes.NewBufferString(body))
|
||||
r.SetPathValue("id", "bad-id")
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Update(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseUpdate_InvalidJSON(t *testing.T) {
|
||||
h := &CaseHandler{}
|
||||
caseID := uuid.New().String()
|
||||
r := httptest.NewRequest("PUT", "/api/cases/"+caseID, bytes.NewBufferString(`{bad`))
|
||||
r.SetPathValue("id", caseID)
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Update(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseDelete_NoTenant(t *testing.T) {
|
||||
h := &CaseHandler{}
|
||||
r := httptest.NewRequest("DELETE", "/api/cases/"+uuid.New().String(), nil)
|
||||
r.SetPathValue("id", uuid.New().String())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Delete(w, r)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected 403, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseDelete_InvalidID(t *testing.T) {
|
||||
h := &CaseHandler{}
|
||||
r := httptest.NewRequest("DELETE", "/api/cases/bad-id", nil)
|
||||
r.SetPathValue("id", "bad-id")
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Delete(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
19
backend/internal/handlers/dashboard_handler_test.go
Normal file
19
backend/internal/handlers/dashboard_handler_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDashboardGet_NoTenant(t *testing.T) {
|
||||
h := &DashboardHandler{}
|
||||
r := httptest.NewRequest("GET", "/api/dashboard", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Get(w, r)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected 403, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
166
backend/internal/handlers/document_handler_test.go
Normal file
166
backend/internal/handlers/document_handler_test.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"mgit.msbls.de/m/KanzlAI-mGMT/internal/auth"
|
||||
)
|
||||
|
||||
func TestDocumentListByCase_NoTenant(t *testing.T) {
|
||||
h := &DocumentHandler{}
|
||||
r := httptest.NewRequest("GET", "/api/cases/"+uuid.New().String()+"/documents", nil)
|
||||
r.SetPathValue("id", uuid.New().String())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.ListByCase(w, r)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected 403, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentListByCase_InvalidCaseID(t *testing.T) {
|
||||
h := &DocumentHandler{}
|
||||
r := httptest.NewRequest("GET", "/api/cases/bad-id/documents", nil)
|
||||
r.SetPathValue("id", "bad-id")
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.ListByCase(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentUpload_NoTenant(t *testing.T) {
|
||||
h := &DocumentHandler{}
|
||||
r := httptest.NewRequest("POST", "/api/cases/"+uuid.New().String()+"/documents", nil)
|
||||
r.SetPathValue("id", uuid.New().String())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Upload(w, r)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected 403, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentUpload_InvalidCaseID(t *testing.T) {
|
||||
h := &DocumentHandler{}
|
||||
r := httptest.NewRequest("POST", "/api/cases/bad-id/documents", nil)
|
||||
r.SetPathValue("id", "bad-id")
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Upload(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDownload_NoTenant(t *testing.T) {
|
||||
h := &DocumentHandler{}
|
||||
r := httptest.NewRequest("GET", "/api/documents/"+uuid.New().String(), nil)
|
||||
r.SetPathValue("docId", uuid.New().String())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Download(w, r)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected 403, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDownload_InvalidID(t *testing.T) {
|
||||
h := &DocumentHandler{}
|
||||
r := httptest.NewRequest("GET", "/api/documents/bad-id", nil)
|
||||
r.SetPathValue("docId", "bad-id")
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Download(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentGetMeta_NoTenant(t *testing.T) {
|
||||
h := &DocumentHandler{}
|
||||
r := httptest.NewRequest("GET", "/api/documents/"+uuid.New().String()+"/meta", nil)
|
||||
r.SetPathValue("docId", uuid.New().String())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.GetMeta(w, r)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected 403, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentGetMeta_InvalidID(t *testing.T) {
|
||||
h := &DocumentHandler{}
|
||||
r := httptest.NewRequest("GET", "/api/documents/bad-id/meta", nil)
|
||||
r.SetPathValue("docId", "bad-id")
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.GetMeta(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDelete_NoTenant(t *testing.T) {
|
||||
h := &DocumentHandler{}
|
||||
r := httptest.NewRequest("DELETE", "/api/documents/"+uuid.New().String(), nil)
|
||||
r.SetPathValue("docId", uuid.New().String())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Delete(w, r)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected 403, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDelete_InvalidID(t *testing.T) {
|
||||
h := &DocumentHandler{}
|
||||
r := httptest.NewRequest("DELETE", "/api/documents/bad-id", nil)
|
||||
r.SetPathValue("docId", "bad-id")
|
||||
ctx := auth.ContextWithTenantID(
|
||||
auth.ContextWithUserID(r.Context(), uuid.New()),
|
||||
uuid.New(),
|
||||
)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.Delete(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
@@ -73,9 +73,31 @@ func createTestJWT(t *testing.T, userID uuid.UUID) string {
|
||||
return signed
|
||||
}
|
||||
|
||||
// createExpiredJWT creates an expired JWT for testing auth rejection.
|
||||
func createExpiredJWT(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().Add(-2 * time.Hour).Unix(),
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
signed, err := token.SignedString([]byte(secret))
|
||||
if err != nil {
|
||||
t.Fatalf("signing expired 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) {
|
||||
func setupTestTenant(t *testing.T, _ http.Handler) (tenantID, userID uuid.UUID) {
|
||||
t.Helper()
|
||||
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
@@ -127,6 +149,43 @@ func setupTestTenant(t *testing.T, handler http.Handler) (tenantID, userID uuid.
|
||||
return tenantID, userID
|
||||
}
|
||||
|
||||
// doRequest is a test helper that makes an HTTP request to the test handler.
|
||||
func doRequest(t *testing.T, handler http.Handler, method, path, token string, body any) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
|
||||
var bodyReader *bytes.Buffer
|
||||
if body != nil {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
t.Fatalf("marshaling request body: %v", err)
|
||||
}
|
||||
bodyReader = bytes.NewBuffer(b)
|
||||
} else {
|
||||
bodyReader = &bytes.Buffer{}
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(method, path, bodyReader)
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
// decodeJSON decodes the response body into the given target.
|
||||
func decodeJSON(t *testing.T, w *httptest.ResponseRecorder, target any) {
|
||||
t.Helper()
|
||||
if err := json.Unmarshal(w.Body.Bytes(), target); err != nil {
|
||||
t.Fatalf("decoding JSON response: %v (body: %s)", err, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ── Health ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestHealthEndpoint(t *testing.T) {
|
||||
handler, cleanup := testServer(t)
|
||||
defer cleanup()
|
||||
@@ -146,6 +205,810 @@ func TestHealthEndpoint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Auth Middleware ──────────────────────────────────────────────────────────
|
||||
|
||||
func TestUnauthenticatedAccess(t *testing.T) {
|
||||
handler, cleanup := testServer(t)
|
||||
defer cleanup()
|
||||
|
||||
endpoints := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{"GET", "/api/cases"},
|
||||
{"POST", "/api/cases"},
|
||||
{"GET", "/api/tenants"},
|
||||
{"GET", "/api/deadlines"},
|
||||
{"GET", "/api/appointments"},
|
||||
{"GET", "/api/dashboard"},
|
||||
}
|
||||
|
||||
for _, ep := range endpoints {
|
||||
t.Run(ep.method+" "+ep.path, func(t *testing.T) {
|
||||
w := doRequest(t, handler, ep.method, ep.path, "", nil)
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpiredJWT(t *testing.T) {
|
||||
handler, cleanup := testServer(t)
|
||||
defer cleanup()
|
||||
|
||||
token := createExpiredJWT(t, uuid.New())
|
||||
w := doRequest(t, handler, "GET", "/api/cases", token, nil)
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401 for expired token, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidJWT(t *testing.T) {
|
||||
handler, cleanup := testServer(t)
|
||||
defer cleanup()
|
||||
|
||||
w := doRequest(t, handler, "GET", "/api/cases", "totally-not-a-jwt", nil)
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401 for invalid token, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrongSecretJWT(t *testing.T) {
|
||||
handler, cleanup := testServer(t)
|
||||
defer cleanup()
|
||||
|
||||
// Sign with a different secret
|
||||
claims := jwt.MapClaims{
|
||||
"sub": uuid.New().String(),
|
||||
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
signed, _ := token.SignedString([]byte("wrong-secret-key"))
|
||||
|
||||
w := doRequest(t, handler, "GET", "/api/cases", signed, nil)
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401 for wrong-secret token, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tenant CRUD ─────────────────────────────────────────────────────────────
|
||||
|
||||
func TestTenantCRUD(t *testing.T) {
|
||||
handler, cleanup := testServer(t)
|
||||
defer cleanup()
|
||||
|
||||
_, userID := setupTestTenant(t, handler)
|
||||
token := createTestJWT(t, userID)
|
||||
|
||||
// List tenants — should have the one from setup
|
||||
t.Run("ListTenants", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/tenants", token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var tenants []map[string]any
|
||||
decodeJSON(t, w, &tenants)
|
||||
if len(tenants) < 1 {
|
||||
t.Fatalf("expected at least 1 tenant, got %d", len(tenants))
|
||||
}
|
||||
})
|
||||
|
||||
// Create a new tenant
|
||||
var newTenantID string
|
||||
t.Run("CreateTenant", func(t *testing.T) {
|
||||
body := map[string]string{"name": "Test Kanzlei Neu", "slug": "test-neu-" + uuid.New().String()[:8]}
|
||||
w := doRequest(t, handler, "POST", "/api/tenants", token, body)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]any
|
||||
decodeJSON(t, w, &resp)
|
||||
newTenantID, _ = resp["id"].(string)
|
||||
if newTenantID == "" {
|
||||
t.Fatal("created tenant missing ID")
|
||||
}
|
||||
|
||||
// Cleanup the extra tenant
|
||||
t.Cleanup(func() {
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
cleanupDB, _ := db.Connect(dbURL)
|
||||
if cleanupDB != nil {
|
||||
cleanupDB.Exec(`DELETE FROM user_tenants WHERE tenant_id = $1`, newTenantID)
|
||||
cleanupDB.Exec(`DELETE FROM tenants WHERE id = $1`, newTenantID)
|
||||
cleanupDB.Close()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Get tenant by ID
|
||||
t.Run("GetTenant", func(t *testing.T) {
|
||||
if newTenantID == "" {
|
||||
t.Skip("no tenant ID from create test")
|
||||
}
|
||||
w := doRequest(t, handler, "GET", "/api/tenants/"+newTenantID, token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
// Get tenant with invalid ID
|
||||
t.Run("GetTenant_InvalidID", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/tenants/not-a-uuid", token, nil)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
// Create tenant missing fields
|
||||
t.Run("CreateTenant_MissingFields", func(t *testing.T) {
|
||||
body := map[string]string{"name": "", "slug": ""}
|
||||
w := doRequest(t, handler, "POST", "/api/tenants", token, body)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── Case CRUD ───────────────────────────────────────────────────────────────
|
||||
|
||||
func TestCaseCRUD(t *testing.T) {
|
||||
handler, cleanup := testServer(t)
|
||||
defer cleanup()
|
||||
|
||||
_, userID := setupTestTenant(t, handler)
|
||||
token := createTestJWT(t, userID)
|
||||
|
||||
var caseID string
|
||||
|
||||
// Create case
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
body := map[string]string{
|
||||
"case_number": "TEST/CRUD/001",
|
||||
"title": "CRUD Test — Patentverletzung",
|
||||
"case_type": "patent",
|
||||
"court": "UPC München",
|
||||
}
|
||||
w := doRequest(t, handler, "POST", "/api/cases", token, body)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]any
|
||||
decodeJSON(t, w, &resp)
|
||||
caseID, _ = resp["id"].(string)
|
||||
if caseID == "" {
|
||||
t.Fatal("created case missing ID")
|
||||
}
|
||||
if resp["status"] != "active" {
|
||||
t.Errorf("expected default status active, got %v", resp["status"])
|
||||
}
|
||||
})
|
||||
|
||||
// Create case - missing fields
|
||||
t.Run("Create_MissingFields", func(t *testing.T) {
|
||||
body := map[string]string{"title": ""}
|
||||
w := doRequest(t, handler, "POST", "/api/cases", token, body)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
// List cases
|
||||
t.Run("List", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/cases", token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]any
|
||||
decodeJSON(t, w, &resp)
|
||||
total := resp["total"].(float64)
|
||||
if total < 1 {
|
||||
t.Fatalf("expected at least 1 case, got %.0f", total)
|
||||
}
|
||||
})
|
||||
|
||||
// List with search filter
|
||||
t.Run("List_SearchFilter", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/cases?search=CRUD", token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
var resp map[string]any
|
||||
decodeJSON(t, w, &resp)
|
||||
total := resp["total"].(float64)
|
||||
if total < 1 {
|
||||
t.Error("search filter should find our test case")
|
||||
}
|
||||
})
|
||||
|
||||
// List with status filter
|
||||
t.Run("List_StatusFilter", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/cases?status=active", token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
// Get case by ID
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
if caseID == "" {
|
||||
t.Skip("no case ID")
|
||||
}
|
||||
w := doRequest(t, handler, "GET", "/api/cases/"+caseID, token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]any
|
||||
decodeJSON(t, w, &resp)
|
||||
if resp["title"] != "CRUD Test — Patentverletzung" {
|
||||
t.Errorf("unexpected title: %v", resp["title"])
|
||||
}
|
||||
// CaseDetail should include parties and recent_events
|
||||
if _, ok := resp["parties"]; !ok {
|
||||
t.Error("missing parties in CaseDetail response")
|
||||
}
|
||||
if _, ok := resp["recent_events"]; !ok {
|
||||
t.Error("missing recent_events in CaseDetail response")
|
||||
}
|
||||
})
|
||||
|
||||
// Get case - invalid ID
|
||||
t.Run("Get_InvalidID", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/cases/not-a-uuid", token, nil)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
// Get case - not found
|
||||
t.Run("Get_NotFound", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/cases/"+uuid.New().String(), token, nil)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d", w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
// Update case
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
if caseID == "" {
|
||||
t.Skip("no case ID")
|
||||
}
|
||||
body := map[string]string{"title": "Updated Title", "court": "UPC Paris"}
|
||||
w := doRequest(t, handler, "PUT", "/api/cases/"+caseID, token, body)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]any
|
||||
decodeJSON(t, w, &resp)
|
||||
if resp["title"] != "Updated Title" {
|
||||
t.Errorf("title not updated: %v", resp["title"])
|
||||
}
|
||||
})
|
||||
|
||||
// Delete (archive) case
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
if caseID == "" {
|
||||
t.Skip("no case ID")
|
||||
}
|
||||
w := doRequest(t, handler, "DELETE", "/api/cases/"+caseID, token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]string
|
||||
decodeJSON(t, w, &resp)
|
||||
if resp["status"] != "archived" {
|
||||
t.Errorf("expected archived status, got %s", resp["status"])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── Deadline CRUD ───────────────────────────────────────────────────────────
|
||||
|
||||
func TestDeadlineCRUD(t *testing.T) {
|
||||
handler, cleanup := testServer(t)
|
||||
defer cleanup()
|
||||
|
||||
_, userID := setupTestTenant(t, handler)
|
||||
token := createTestJWT(t, userID)
|
||||
|
||||
// First create a case to attach deadlines to
|
||||
caseBody := map[string]string{
|
||||
"case_number": "TEST/DL/001",
|
||||
"title": "Deadline Test Case",
|
||||
}
|
||||
w := doRequest(t, handler, "POST", "/api/cases", token, caseBody)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("create case: expected 201, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var caseResp map[string]any
|
||||
decodeJSON(t, w, &caseResp)
|
||||
caseID := caseResp["id"].(string)
|
||||
|
||||
var deadlineID string
|
||||
dueDate := time.Now().AddDate(0, 0, 14).Format("2006-01-02")
|
||||
warnDate := time.Now().AddDate(0, 0, 10).Format("2006-01-02")
|
||||
|
||||
// Create deadline
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
body := map[string]string{
|
||||
"title": "Klageerwiderung einreichen",
|
||||
"due_date": dueDate,
|
||||
"warning_date": warnDate,
|
||||
"source": "manual",
|
||||
}
|
||||
w := doRequest(t, handler, "POST", "/api/cases/"+caseID+"/deadlines", token, body)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]any
|
||||
decodeJSON(t, w, &resp)
|
||||
deadlineID, _ = resp["id"].(string)
|
||||
if deadlineID == "" {
|
||||
t.Fatal("created deadline missing ID")
|
||||
}
|
||||
if resp["status"] != "pending" {
|
||||
t.Errorf("expected status pending, got %v", resp["status"])
|
||||
}
|
||||
})
|
||||
|
||||
// Create deadline - missing fields
|
||||
t.Run("Create_MissingFields", func(t *testing.T) {
|
||||
body := map[string]string{"title": ""}
|
||||
w := doRequest(t, handler, "POST", "/api/cases/"+caseID+"/deadlines", token, body)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
// List all deadlines
|
||||
t.Run("ListAll", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/deadlines", token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
// List deadlines for case
|
||||
t.Run("ListForCase", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/cases/"+caseID+"/deadlines", token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var deadlines []any
|
||||
decodeJSON(t, w, &deadlines)
|
||||
if len(deadlines) < 1 {
|
||||
t.Fatal("expected at least 1 deadline")
|
||||
}
|
||||
})
|
||||
|
||||
// Update deadline
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
if deadlineID == "" {
|
||||
t.Skip("no deadline ID")
|
||||
}
|
||||
newTitle := "Aktualisierte Frist"
|
||||
body := map[string]*string{"title": &newTitle}
|
||||
w := doRequest(t, handler, "PUT", "/api/deadlines/"+deadlineID, token, body)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]any
|
||||
decodeJSON(t, w, &resp)
|
||||
if resp["title"] != newTitle {
|
||||
t.Errorf("expected title %q, got %v", newTitle, resp["title"])
|
||||
}
|
||||
})
|
||||
|
||||
// Complete deadline
|
||||
t.Run("Complete", func(t *testing.T) {
|
||||
if deadlineID == "" {
|
||||
t.Skip("no deadline ID")
|
||||
}
|
||||
w := doRequest(t, handler, "PATCH", "/api/deadlines/"+deadlineID+"/complete", token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]any
|
||||
decodeJSON(t, w, &resp)
|
||||
if resp["status"] != "completed" {
|
||||
t.Errorf("expected status completed, got %v", resp["status"])
|
||||
}
|
||||
if resp["completed_at"] == nil {
|
||||
t.Error("expected completed_at to be set")
|
||||
}
|
||||
})
|
||||
|
||||
// Delete deadline
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
if deadlineID == "" {
|
||||
t.Skip("no deadline ID")
|
||||
}
|
||||
w := doRequest(t, handler, "DELETE", "/api/deadlines/"+deadlineID, token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
// Delete non-existent deadline
|
||||
t.Run("Delete_NotFound", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "DELETE", "/api/deadlines/"+uuid.New().String(), token, nil)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d", w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── Appointment CRUD ────────────────────────────────────────────────────────
|
||||
|
||||
func TestAppointmentCRUD(t *testing.T) {
|
||||
handler, cleanup := testServer(t)
|
||||
defer cleanup()
|
||||
|
||||
_, userID := setupTestTenant(t, handler)
|
||||
token := createTestJWT(t, userID)
|
||||
|
||||
startAt := time.Now().Add(48 * time.Hour).UTC().Truncate(time.Second)
|
||||
endAt := startAt.Add(2 * time.Hour)
|
||||
|
||||
var appointmentID string
|
||||
|
||||
// Create appointment
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
body := map[string]any{
|
||||
"title": "Mündliche Verhandlung",
|
||||
"start_at": startAt.Format(time.RFC3339),
|
||||
"end_at": endAt.Format(time.RFC3339),
|
||||
"location": "UPC München, Saal 3",
|
||||
"appointment_type": "hearing",
|
||||
}
|
||||
w := doRequest(t, handler, "POST", "/api/appointments", token, body)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]any
|
||||
decodeJSON(t, w, &resp)
|
||||
appointmentID, _ = resp["id"].(string)
|
||||
if appointmentID == "" {
|
||||
t.Fatal("created appointment missing ID")
|
||||
}
|
||||
})
|
||||
|
||||
// Create appointment - missing title
|
||||
t.Run("Create_MissingTitle", func(t *testing.T) {
|
||||
body := map[string]any{
|
||||
"start_at": startAt.Format(time.RFC3339),
|
||||
}
|
||||
w := doRequest(t, handler, "POST", "/api/appointments", token, body)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
// Create appointment - missing start_at
|
||||
t.Run("Create_MissingStartAt", func(t *testing.T) {
|
||||
body := map[string]any{"title": "Test"}
|
||||
w := doRequest(t, handler, "POST", "/api/appointments", token, body)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
// List appointments
|
||||
t.Run("List", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/appointments", token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var appointments []any
|
||||
decodeJSON(t, w, &appointments)
|
||||
if len(appointments) < 1 {
|
||||
t.Fatal("expected at least 1 appointment")
|
||||
}
|
||||
})
|
||||
|
||||
// Update appointment
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
if appointmentID == "" {
|
||||
t.Skip("no appointment ID")
|
||||
}
|
||||
newStart := startAt.Add(24 * time.Hour)
|
||||
body := map[string]any{
|
||||
"title": "Verlegter Termin",
|
||||
"start_at": newStart.Format(time.RFC3339),
|
||||
"location": "UPC Paris, Saal 1",
|
||||
}
|
||||
w := doRequest(t, handler, "PUT", "/api/appointments/"+appointmentID, token, body)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]any
|
||||
decodeJSON(t, w, &resp)
|
||||
if resp["title"] != "Verlegter Termin" {
|
||||
t.Errorf("title not updated: %v", resp["title"])
|
||||
}
|
||||
})
|
||||
|
||||
// Delete appointment
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
if appointmentID == "" {
|
||||
t.Skip("no appointment ID")
|
||||
}
|
||||
w := doRequest(t, handler, "DELETE", "/api/appointments/"+appointmentID, token, nil)
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Errorf("expected 204, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
// Delete non-existent appointment
|
||||
t.Run("Delete_NotFound", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "DELETE", "/api/appointments/"+uuid.New().String(), token, nil)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d", w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── Dashboard ───────────────────────────────────────────────────────────────
|
||||
|
||||
func TestDashboard(t *testing.T) {
|
||||
handler, cleanup := testServer(t)
|
||||
defer cleanup()
|
||||
|
||||
_, userID := setupTestTenant(t, handler)
|
||||
token := createTestJWT(t, userID)
|
||||
|
||||
// Create a case + deadline so dashboard has data
|
||||
caseBody := map[string]string{
|
||||
"case_number": "TEST/DASH/001",
|
||||
"title": "Dashboard Test Case",
|
||||
}
|
||||
w := doRequest(t, handler, "POST", "/api/cases", token, caseBody)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("create case: %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var caseResp map[string]any
|
||||
decodeJSON(t, w, &caseResp)
|
||||
caseID := caseResp["id"].(string)
|
||||
|
||||
dueDate := time.Now().AddDate(0, 0, 3).Format("2006-01-02")
|
||||
dlBody := map[string]string{
|
||||
"title": "Dashboard Test Deadline",
|
||||
"due_date": dueDate,
|
||||
}
|
||||
w = doRequest(t, handler, "POST", "/api/cases/"+caseID+"/deadlines", token, dlBody)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("create deadline: %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/dashboard", token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var dashboard map[string]any
|
||||
decodeJSON(t, w, &dashboard)
|
||||
|
||||
// Verify all expected sections exist
|
||||
requiredKeys := []string{"deadline_summary", "case_summary", "upcoming_deadlines", "upcoming_appointments", "recent_activity"}
|
||||
for _, key := range requiredKeys {
|
||||
if _, exists := dashboard[key]; !exists {
|
||||
t.Errorf("missing key %q in dashboard", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify case_summary has at least 1 active case
|
||||
if cs, ok := dashboard["case_summary"].(map[string]any); ok {
|
||||
if active, ok := cs["active_count"].(float64); ok && active < 1 {
|
||||
t.Errorf("expected at least 1 active case, got %.0f", active)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify deadline_summary fields
|
||||
if ds, ok := dashboard["deadline_summary"].(map[string]any); ok {
|
||||
for _, key := range []string{"overdue_count", "due_this_week", "due_next_week", "ok_count"} {
|
||||
if _, exists := ds[key]; !exists {
|
||||
t.Errorf("missing deadline_summary.%s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── Deadline Calculator ─────────────────────────────────────────────────────
|
||||
|
||||
func TestDeadlineCalculator(t *testing.T) {
|
||||
handler, cleanup := testServer(t)
|
||||
defer cleanup()
|
||||
|
||||
_, userID := setupTestTenant(t, handler)
|
||||
token := createTestJWT(t, userID)
|
||||
|
||||
t.Run("Calculate_ValidRequest", func(t *testing.T) {
|
||||
body := map[string]any{
|
||||
"proceeding_type": "INF",
|
||||
"trigger_event_date": "2026-06-01",
|
||||
}
|
||||
w := doRequest(t, handler, "POST", "/api/deadlines/calculate", token, body)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]any
|
||||
decodeJSON(t, w, &resp)
|
||||
if resp["proceeding_type"] != "INF" {
|
||||
t.Errorf("expected INF, got %v", resp["proceeding_type"])
|
||||
}
|
||||
deadlines, ok := resp["deadlines"].([]any)
|
||||
if !ok {
|
||||
t.Fatal("deadlines not an array")
|
||||
}
|
||||
if len(deadlines) == 0 {
|
||||
t.Error("expected at least 1 calculated deadline")
|
||||
}
|
||||
|
||||
// Verify each deadline has expected fields
|
||||
for i, dl := range deadlines {
|
||||
m, ok := dl.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, key := range []string{"rule_code", "title", "due_date"} {
|
||||
if _, exists := m[key]; !exists {
|
||||
t.Errorf("deadline[%d] missing field %q", i, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Calculate_MissingFields", func(t *testing.T) {
|
||||
body := map[string]string{}
|
||||
w := doRequest(t, handler, "POST", "/api/deadlines/calculate", token, body)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Calculate_InvalidDateFormat", func(t *testing.T) {
|
||||
body := map[string]string{
|
||||
"proceeding_type": "INF",
|
||||
"trigger_event_date": "01/06/2026",
|
||||
}
|
||||
w := doRequest(t, handler, "POST", "/api/deadlines/calculate", token, body)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 for bad date format, got %d", w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Calculate_UnknownProceedingType", func(t *testing.T) {
|
||||
body := map[string]string{
|
||||
"proceeding_type": "NONEXISTENT",
|
||||
"trigger_event_date": "2026-06-01",
|
||||
}
|
||||
w := doRequest(t, handler, "POST", "/api/deadlines/calculate", token, body)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 for unknown type, got %d", w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── Proceeding Types & Deadline Rules ───────────────────────────────────────
|
||||
|
||||
func TestProceedingTypesAndRules(t *testing.T) {
|
||||
handler, cleanup := testServer(t)
|
||||
defer cleanup()
|
||||
|
||||
_, userID := setupTestTenant(t, handler)
|
||||
token := createTestJWT(t, userID)
|
||||
|
||||
t.Run("ListProceedingTypes", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/proceeding-types", token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var types []any
|
||||
decodeJSON(t, w, &types)
|
||||
if len(types) == 0 {
|
||||
t.Error("expected at least 1 proceeding type")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListDeadlineRules", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/deadline-rules", token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetRuleTree_INF", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/deadline-rules/INF", token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var tree []any
|
||||
decodeJSON(t, w, &tree)
|
||||
if len(tree) == 0 {
|
||||
t.Error("expected non-empty rule tree for INF")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── Document Upload ─────────────────────────────────────────────────────────
|
||||
|
||||
func TestDocumentUpload(t *testing.T) {
|
||||
handler, cleanup := testServer(t)
|
||||
defer cleanup()
|
||||
|
||||
_, userID := setupTestTenant(t, handler)
|
||||
token := createTestJWT(t, userID)
|
||||
|
||||
// Create a case first
|
||||
caseBody := map[string]string{
|
||||
"case_number": "TEST/DOC/001",
|
||||
"title": "Document Test Case",
|
||||
}
|
||||
w := doRequest(t, handler, "POST", "/api/cases", token, caseBody)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("create case: %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var caseResp map[string]any
|
||||
decodeJSON(t, w, &caseResp)
|
||||
caseID := caseResp["id"].(string)
|
||||
|
||||
t.Run("ListDocuments_Empty", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/cases/"+caseID+"/documents", token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]any
|
||||
decodeJSON(t, w, &resp)
|
||||
total := resp["total"].(float64)
|
||||
if total != 0 {
|
||||
t.Errorf("expected 0 documents, got %.0f", total)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetMeta_NotFound", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/documents/"+uuid.New().String()+"/meta", token, nil)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d", w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetMeta_InvalidID", func(t *testing.T) {
|
||||
w := doRequest(t, handler, "GET", "/api/documents/not-a-uuid/meta", token, nil)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── AI Endpoints ────────────────────────────────────────────────────────────
|
||||
|
||||
func TestAIExtractDeadlines_NoAPIKey(t *testing.T) {
|
||||
// AI endpoints are only registered when ANTHROPIC_API_KEY is set.
|
||||
// Without it, the route should 404.
|
||||
handler, cleanup := testServer(t)
|
||||
defer cleanup()
|
||||
|
||||
_, userID := setupTestTenant(t, handler)
|
||||
token := createTestJWT(t, userID)
|
||||
|
||||
body := map[string]string{"text": "Die Frist beträgt 3 Monate ab Zustellung."}
|
||||
w := doRequest(t, handler, "POST", "/api/ai/extract-deadlines", token, body)
|
||||
|
||||
// Should be 404 (route not registered) since we don't set ANTHROPIC_API_KEY
|
||||
// or 200 if the key happens to be set in the env
|
||||
if w.Code == http.StatusNotFound {
|
||||
t.Log("AI endpoint correctly not available without API key")
|
||||
} else if w.Code == http.StatusOK {
|
||||
t.Log("AI endpoint available (API key configured in env)")
|
||||
} else {
|
||||
t.Logf("AI endpoint returned %d (may need API key)", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Critical Path E2E ───────────────────────────────────────────────────────
|
||||
|
||||
func TestCriticalPath_CreateCase_AddDeadline_Dashboard(t *testing.T) {
|
||||
handler, cleanup := testServer(t)
|
||||
defer cleanup()
|
||||
@@ -165,7 +1028,7 @@ func TestCriticalPath_CreateCase_AddDeadline_Dashboard(t *testing.T) {
|
||||
t.Fatalf("create case: expected 201, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var createdCase map[string]interface{}
|
||||
var createdCase map[string]any
|
||||
json.Unmarshal(w.Body.Bytes(), &createdCase)
|
||||
caseID, ok := createdCase["id"].(string)
|
||||
if !ok || caseID == "" {
|
||||
@@ -183,7 +1046,7 @@ func TestCriticalPath_CreateCase_AddDeadline_Dashboard(t *testing.T) {
|
||||
t.Fatalf("list cases: expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var caseList map[string]interface{}
|
||||
var caseList map[string]any
|
||||
json.Unmarshal(w.Body.Bytes(), &caseList)
|
||||
total := caseList["total"].(float64)
|
||||
if total < 1 {
|
||||
@@ -204,7 +1067,7 @@ func TestCriticalPath_CreateCase_AddDeadline_Dashboard(t *testing.T) {
|
||||
t.Fatalf("create deadline: expected 201, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var createdDeadline map[string]interface{}
|
||||
var createdDeadline map[string]any
|
||||
json.Unmarshal(w.Body.Bytes(), &createdDeadline)
|
||||
deadlineID, ok := createdDeadline["id"].(string)
|
||||
if !ok || deadlineID == "" {
|
||||
@@ -212,7 +1075,22 @@ func TestCriticalPath_CreateCase_AddDeadline_Dashboard(t *testing.T) {
|
||||
}
|
||||
t.Logf("Created deadline: %s", deadlineID)
|
||||
|
||||
// Step 4: Verify deadline appears in case deadlines
|
||||
// Step 4: Create an appointment linked to the case
|
||||
startAt := time.Now().Add(72 * time.Hour).UTC().Truncate(time.Second)
|
||||
apptBody := map[string]any{
|
||||
"title": "Mündliche Verhandlung",
|
||||
"start_at": startAt.Format(time.RFC3339),
|
||||
"location": "UPC München",
|
||||
"appointment_type": "hearing",
|
||||
"case_id": caseID,
|
||||
}
|
||||
w = doRequest(t, handler, "POST", "/api/appointments", token, apptBody)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("create appointment: expected 201, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
t.Log("Created appointment linked to case")
|
||||
|
||||
// Step 5: Verify deadline appears in case deadlines
|
||||
req = httptest.NewRequest("GET", "/api/cases/"+caseID+"/deadlines", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w = httptest.NewRecorder()
|
||||
@@ -222,13 +1100,13 @@ func TestCriticalPath_CreateCase_AddDeadline_Dashboard(t *testing.T) {
|
||||
t.Fatalf("list deadlines: expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var deadlines []interface{}
|
||||
var deadlines []any
|
||||
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
|
||||
// Step 6: Fetch dashboard — should include our case and deadline
|
||||
req = httptest.NewRequest("GET", "/api/dashboard", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w = httptest.NewRecorder()
|
||||
@@ -238,7 +1116,7 @@ func TestCriticalPath_CreateCase_AddDeadline_Dashboard(t *testing.T) {
|
||||
t.Fatalf("dashboard: expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var dashboard map[string]interface{}
|
||||
var dashboard map[string]any
|
||||
json.Unmarshal(w.Body.Bytes(), &dashboard)
|
||||
|
||||
// Verify dashboard has expected sections
|
||||
@@ -249,25 +1127,22 @@ func TestCriticalPath_CreateCase_AddDeadline_Dashboard(t *testing.T) {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if cs, ok := dashboard["case_summary"].(map[string]any); ok {
|
||||
if active, ok := cs["active_count"].(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())
|
||||
// Step 7: Complete the deadline and verify
|
||||
w = doRequest(t, handler, "PATCH", "/api/deadlines/"+deadlineID+"/complete", token, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("complete deadline: expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var completedDL map[string]any
|
||||
decodeJSON(t, w, &completedDL)
|
||||
if completedDL["status"] != "completed" {
|
||||
t.Errorf("expected completed, got %v", completedDL["status"])
|
||||
}
|
||||
|
||||
t.Logf("Full critical path passed: auth -> create case -> add deadline -> create appointment -> dashboard -> complete deadline")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user