Files
2026-09-16 06:26:16 +02:00

272 lines
8.9 KiB
Go

package httpserver
import (
"encoding/json"
"io"
"log"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"sync"
"testing"
"github.com/example/notify-gateway/internal/config"
"github.com/example/notify-gateway/internal/divera"
)
func TestExtractCatalogFromKeyedPullData(t *testing.T) {
raw := map[string]any{
"data": map[string]any{
"cluster": map[string]any{"101": map[string]any{"title": "Löschzug 1"}},
"group": map[string]any{"202": map[string]any{"title": "Atemschutz"}},
"user_cluster_relation": map[string]any{"303": map[string]any{"firstname": "Max", "lastname": "Muster"}},
"vehicle": map[string]any{"404": map[string]any{"name": "HLF 20", "ric": "1234567"}},
},
}
got := extractCatalog(raw)
if len(got.Units) != 1 || got.Units[0].ID != 101 || got.Units[0].Label != "Löschzug 1" {
t.Fatalf("units = %#v", got.Units)
}
if len(got.Groups) != 1 || got.Groups[0].ID != 202 {
t.Fatalf("groups = %#v", got.Groups)
}
if len(got.Persons) != 1 || got.Persons[0].ID != 303 || got.Persons[0].Label != "Max Muster" {
t.Fatalf("persons = %#v", got.Persons)
}
if len(got.Vehicles) != 1 || got.Vehicles[0].ID != 404 {
t.Fatalf("vehicles = %#v", got.Vehicles)
}
}
func TestExtractCatalogPrefersUCRCollectionKeyOverGlobalUserID(t *testing.T) {
// Divera247 documents cluster.consumer as the user master-data collection.
// It can be keyed by UCR while a nested id refers to the global User-ID.
// Alarm recipients need the UCR key (9876).
raw := map[string]any{
"data": map[string]any{
"cluster": map[string]any{
"id": 42,
"consumer": map[string]any{
"9876": map[string]any{
"id": 1234,
"firstname": "Erika",
"lastname": "Mustermann",
"email": "erika@example.invalid",
},
},
},
},
}
got := extractCatalog(raw)
if len(got.Persons) != 1 {
t.Fatalf("persons = %#v", got.Persons)
}
if got.Persons[0].ID != 9876 {
t.Fatalf("expected UCR id 9876, got %#v", got.Persons[0])
}
if got.Persons[0].Label != "Erika Mustermann" {
t.Fatalf("unexpected label: %#v", got.Persons[0])
}
}
func TestParseV3PersonsUsesUserClusterRelationID(t *testing.T) {
body := []byte(`[
{"id":303,"cluster_id":42,"foreign_id":"ext-1","user":{"id":999,"firstname":"Max","lastname":"Muster","email":"max@example.invalid"}}
]`)
got, err := parseV3Persons(body)
if err != nil {
t.Fatal(err)
}
if len(got) != 1 || got[0].ID != 303 || got[0].Label != "Max Muster" {
t.Fatalf("persons = %#v", got)
}
if !strings.Contains(got[0].Subtitle, "Einheit 42") {
t.Fatalf("subtitle = %q", got[0].Subtitle)
}
}
func TestExtractUCRRefsFromPullAll(t *testing.T) {
raw := map[string]any{
"data": map[string]any{
"ucr_active": float64(555),
"ucr": []any{
map[string]any{"id": float64(555), "cluster_id": float64(42), "name": "Einheit Nord", "shortname": "N"},
map[string]any{"id": float64(666), "cluster_id": float64(43), "name": "Einheit Süd"},
},
},
}
refs := extractUCRRefs(raw)
if len(refs) != 2 {
t.Fatalf("refs=%#v", refs)
}
if extractActiveUCR(raw) != 555 {
t.Fatalf("active=%d", extractActiveUCR(raw))
}
byID := map[int64]ucrRef{}
for _, ref := range refs {
byID[ref.UCRID] = ref
}
if byID[555].ClusterID != 42 || byID[666].ClusterID != 43 {
t.Fatalf("refs=%#v", refs)
}
}
func TestDiveraCatalogLoadsConsumersAcrossV2UCRsWithoutV3(t *testing.T) {
var mu sync.Mutex
pullCalls := []string{}
v3Calls := 0
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v2/pull/all":
mu.Lock()
pullCalls = append(pullCalls, r.URL.Query().Get("ucr"))
mu.Unlock()
if r.URL.Query().Get("accesskey") != "test-key" {
t.Fatalf("missing accesskey: %s", r.URL.RawQuery)
}
switch r.URL.Query().Get("ucr") {
case "555":
_, _ = io.WriteString(w, `{"data":{"ucr_active":555,"ucr":[{"id":555,"cluster_id":42,"name":"Einheit 42"},{"id":666,"cluster_id":43,"name":"Einheit 43"}],"cluster":{"id":42,"consumer":{"777":{"id":12,"firstname":"Lisa","lastname":"Beispiel","email":"lisa@example.invalid"}},"group":{"11":{"title":"Gruppe Nord"}},"vehicle":{}}}}`)
case "666":
_, _ = io.WriteString(w, `{"data":{"ucr_active":666,"ucr":[{"id":555,"cluster_id":42,"name":"Einheit 42"},{"id":666,"cluster_id":43,"name":"Einheit 43"}],"cluster":{"id":43,"consumer":{"888":{"id":13,"firstname":"Max","lastname":"Süd"}},"group":{"22":{"title":"Gruppe Süd"}},"vehicle":{}}}}`)
default:
http.Error(w, "unexpected ucr", http.StatusBadRequest)
}
case "/api/v3/user-cluster-relations":
v3Calls++
http.Error(w, `{"message":"Nicht autorisiert"}`, http.StatusForbidden)
default:
http.NotFound(w, r)
}
}))
defer upstream.Close()
store, err := config.Open(filepath.Join(t.TempDir(), "config.json"))
if err != nil {
t.Fatal(err)
}
if err := store.Update(func(c *config.Config) error {
c.Divera.BaseURL = upstream.URL
c.Divera.AccessKey = "test-key"
c.Divera.UCR = 555
c.Divera.DryRun = true
return nil
}); err != nil {
t.Fatal(err)
}
client := divera.New(func() config.DiveraConfig { return store.Get().Divera })
srv := New(store, nil, client, log.New(io.Discard, "", 0))
req := httptest.NewRequest(http.MethodGet, "/ui/api/divera247/catalog", nil)
rr := httptest.NewRecorder()
srv.apiDiveraCatalog(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rr.Code, rr.Body.String())
}
var out struct {
Catalog diveraCatalog `json:"catalog"`
Warnings []string `json:"warnings"`
Diagnostics struct {
V2Calls int `json:"v2_ucr_calls"`
V3Calls int `json:"v3_user_calls"`
Source string `json:"person_source"`
} `json:"diagnostics"`
}
if err := json.Unmarshal(rr.Body.Bytes(), &out); err != nil {
t.Fatal(err)
}
if len(out.Catalog.Persons) != 2 {
t.Fatalf("persons=%#v warnings=%#v", out.Catalog.Persons, out.Warnings)
}
ids := map[int64]bool{}
for _, person := range out.Catalog.Persons {
ids[person.ID] = true
if !strings.Contains(person.Subtitle, "Cluster-ID") {
t.Fatalf("missing unit subtitle: %#v", person)
}
}
if !ids[777] || !ids[888] {
t.Fatalf("persons=%#v", out.Catalog.Persons)
}
if v3Calls != 0 || out.Diagnostics.V3Calls != 0 {
t.Fatalf("v3 must not be called when v2 consumers exist: upstream=%d diagnostics=%d", v3Calls, out.Diagnostics.V3Calls)
}
if out.Diagnostics.Source != "v2 pull/all · cluster.consumer" {
t.Fatalf("source=%q", out.Diagnostics.Source)
}
mu.Lock()
defer mu.Unlock()
if len(pullCalls) != 2 {
t.Fatalf("pullCalls=%#v", pullCalls)
}
}
func TestDiveraCatalogStopsV3FallbackAfterForbidden(t *testing.T) {
v3Calls := 0
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v2/pull/all":
ucr := r.URL.Query().Get("ucr")
if ucr == "555" {
_, _ = io.WriteString(w, `{"data":{"ucr_active":555,"ucr":[{"id":555,"cluster_id":42,"name":"Einheit 42"},{"id":666,"cluster_id":43,"name":"Einheit 43"}],"cluster":{"id":42,"consumer":{}}}}`)
} else {
_, _ = io.WriteString(w, `{"data":{"ucr_active":666,"ucr":[{"id":555,"cluster_id":42,"name":"Einheit 42"},{"id":666,"cluster_id":43,"name":"Einheit 43"}],"cluster":{"id":43,"consumer":{}}}}`)
}
case "/api/v3/user-cluster-relations":
v3Calls++
w.WriteHeader(http.StatusForbidden)
_, _ = io.WriteString(w, `{"name":"Forbidden","message":"Nicht autorisiert","code":0,"status":403}`)
default:
http.NotFound(w, r)
}
}))
defer upstream.Close()
store, err := config.Open(filepath.Join(t.TempDir(), "config.json"))
if err != nil {
t.Fatal(err)
}
if err := store.Update(func(c *config.Config) error {
c.Divera.BaseURL = upstream.URL
c.Divera.AccessKey = "test-key"
c.Divera.UCR = 555
c.Divera.DryRun = true
return nil
}); err != nil {
t.Fatal(err)
}
client := divera.New(func() config.DiveraConfig { return store.Get().Divera })
srv := New(store, nil, client, log.New(io.Discard, "", 0))
rr := httptest.NewRecorder()
srv.apiDiveraCatalog(rr, httptest.NewRequest(http.MethodGet, "/ui/api/divera247/catalog", nil))
if rr.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rr.Code, rr.Body.String())
}
var out struct {
Warnings []string `json:"warnings"`
}
if err := json.Unmarshal(rr.Body.Bytes(), &out); err != nil {
t.Fatal(err)
}
if v3Calls != 1 {
t.Fatalf("expected one v3 probe, got %d", v3Calls)
}
if len(out.Warnings) != 1 || !strings.Contains(out.Warnings[0], "HTTP 403") {
t.Fatalf("warnings=%#v", out.Warnings)
}
}
func TestEditableConfigDoesNotExposeSessionSecrets(t *testing.T) {
c := config.Default()
c.Server.AdminPasswordHash = "secret-hash"
c.Server.SessionSecret = "session-secret"
b, err := json.Marshal(makeEditable(c))
if err != nil {
t.Fatal(err)
}
s := string(b)
if strings.Contains(s, "secret-hash") || strings.Contains(s, "session-secret") || strings.Contains(s, "admin_password_hash") || strings.Contains(s, "session_secret") {
t.Fatalf("editable config leaked internal auth data: %s", s)
}
}