272 lines
8.9 KiB
Go
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)
|
|
}
|
|
}
|