package httpapi import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "basicstack.de/proofedmesh/services/admin-api/internal/store" ) func newDeps(t *testing.T) Dependencies { t.Helper() return Dependencies{ Store: store.NewMemory(), Now: func() time.Time { return time.Date(2026, 10, 25, 12, 0, 0, 0, time.UTC) }, } } func newMux(deps Dependencies) http.Handler { mux := http.NewServeMux() Mount(mux, deps) // inject a principal for tests return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := WithPrincipal(r.Context(), "test-admin") mux.ServeHTTP(w, r.WithContext(ctx)) }) } func TestList_Empty(t *testing.T) { srv := httptest.NewServer(newMux(newDeps(t))) defer srv.Close() resp, err := http.Get(srv.URL + "/admin/per-diem-rates/") if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != 200 { t.Fatalf("status %d", resp.StatusCode) } } func TestCreateAndGet_JSON(t *testing.T) { deps := newDeps(t) srv := httptest.NewServer(newMux(deps)) defer srv.Close() body := `{ "country":"DE","currency":"EUR", "full_day":"28.00","partial_day_min8h":"14.00", "arrival_day":"14.00","departure_day":"14.00", "valid_from":"2026-01-01T00:00:00Z", "source_regulation":"BMF 2026" }` req, _ := http.NewRequest("POST", srv.URL+"/admin/per-diem-rates/", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { t.Fatalf("expected 201, got %d", resp.StatusCode) } var out map[string]map[string]any _ = json.NewDecoder(resp.Body).Decode(&out) rateID, _ := out["rate"]["rate_id"].(string) if rateID == "" { t.Fatalf("no rate_id in response: %v", out) } // Fetch it back resp2, err := http.Get(srv.URL + "/admin/per-diem-rates/" + rateID) if err != nil { t.Fatal(err) } defer resp2.Body.Close() if resp2.StatusCode != 200 { t.Fatalf("get status %d", resp2.StatusCode) } } func TestCreate_InvalidBody_Rejected(t *testing.T) { srv := httptest.NewServer(newMux(newDeps(t))) defer srv.Close() body := `{"country":"XYZ","currency":"EUR","full_day":"28.00","partial_day_min8h":"14.00","arrival_day":"14.00","departure_day":"14.00","valid_from":"2026-01-01T00:00:00Z","source_regulation":"BMF 2026"}` req, _ := http.NewRequest("POST", srv.URL+"/admin/per-diem-rates/", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != 400 { t.Fatalf("expected 400, got %d", resp.StatusCode) } } func TestGet_NotFound(t *testing.T) { srv := httptest.NewServer(newMux(newDeps(t))) defer srv.Close() resp, err := http.Get(srv.URL + "/admin/per-diem-rates/PDR-999") if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Fatalf("expected 404, got %d", resp.StatusCode) } } func TestRetire_UpdatesValidTo(t *testing.T) { deps := newDeps(t) srv := httptest.NewServer(newMux(deps)) defer srv.Close() createBody := `{"country":"DE","currency":"EUR","full_day":"28.00","partial_day_min8h":"14.00","arrival_day":"14.00","departure_day":"14.00","valid_from":"2026-01-01T00:00:00Z","source_regulation":"BMF 2026"}` req, _ := http.NewRequest("POST", srv.URL+"/admin/per-diem-rates/", strings.NewReader(createBody)) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() var out map[string]map[string]any _ = json.NewDecoder(resp.Body).Decode(&out) rateID := out["rate"]["rate_id"].(string) retireBody := `{"valid_to":"2026-06-30"}` req2, _ := http.NewRequest("POST", srv.URL+"/admin/per-diem-rates/"+rateID+"/retire", strings.NewReader(retireBody)) req2.Header.Set("Content-Type", "application/json") resp2, err := http.DefaultClient.Do(req2) if err != nil { t.Fatal(err) } defer resp2.Body.Close() if resp2.StatusCode != 200 { t.Fatalf("retire: expected 200, got %d", resp2.StatusCode) } var out2 map[string]map[string]any _ = json.NewDecoder(resp2.Body).Decode(&out2) if _, ok := out2["rate"]["valid_to"]; !ok { t.Fatal("expected valid_to to be set after retire") } } func TestOverlap_Rejected(t *testing.T) { srv := httptest.NewServer(newMux(newDeps(t))) defer srv.Close() body := func(validFrom string, validTo *string) string { base := `{"country":"DE","currency":"EUR","full_day":"28.00","partial_day_min8h":"14.00","arrival_day":"14.00","departure_day":"14.00","valid_from":"` + validFrom + `T00:00:00Z","source_regulation":"BMF 2026"` if validTo != nil { base += `,"valid_to":"` + *validTo + `T00:00:00Z"` } return base + "}" } vt := "2026-12-31" req1, _ := http.NewRequest("POST", srv.URL+"/admin/per-diem-rates/", strings.NewReader(body("2026-01-01", &vt))) req1.Header.Set("Content-Type", "application/json") resp1, err := http.DefaultClient.Do(req1) if err != nil { t.Fatal(err) } defer resp1.Body.Close() if resp1.StatusCode != http.StatusCreated { t.Fatalf("first create expected 201, got %d", resp1.StatusCode) } req2, _ := http.NewRequest("POST", srv.URL+"/admin/per-diem-rates/", strings.NewReader(body("2026-12-31", nil))) req2.Header.Set("Content-Type", "application/json") resp2, err := http.DefaultClient.Do(req2) if err != nil { t.Fatal(err) } defer resp2.Body.Close() if resp2.StatusCode != http.StatusConflict { t.Fatalf("overlapping create: expected 409, got %d", resp2.StatusCode) } }