package api import ( "net/http" "strings" "testing" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) // R-951 (security review 2026-10-12): GET /customers, /customers/{id} and /customers/{id}/history answered with NO // credential and returned every customer's name, dashboard address and stored box report. Since v0.148.0 they are // operator/global key only. Red-proof (2026-10-12): with the requireGlobalKey call removed from handleCustomer, // TestCustomersRoutes_NoCredentialIsRefused failed (GET /customers/c1 answered 200). const customersTestName = "Teszt Csalad R935" // newCustomersHandler: customer c1 (key ckey) with one stored report and a host h1 (key hkey) under it; a second // customer c2 (key c2key). func newCustomersHandler(t *testing.T) (*Handler, *store.Store) { t.Helper() h, st := newTwoCustomerHandler(t) if err := st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "hkey"}); err != nil { t.Fatalf("UpsertHost: %v", err) } rr := do(h, http.MethodPost, "/report", "ckey", `{"customer_id":"c1","customer_name":"`+customersTestName+`"}`) if rr.Code != http.StatusOK { t.Fatalf("seed report: status = %d, body=%s", rr.Code, rr.Body.String()) } return h, st } var customersRoutes = []string{"/customers", "/customers/c1", "/customers/c1/history"} // Scenario A — a stranger, no Authorization header. func TestCustomersRoutes_NoCredentialIsRefused(t *testing.T) { h, _ := newCustomersHandler(t) for _, p := range customersRoutes { rr := do(h, http.MethodGet, p, "", "") if rr.Code != http.StatusUnauthorized && rr.Code != http.StatusForbidden { t.Errorf("GET %s with no credential: status = %d, want 401/403", p, rr.Code) } if strings.Contains(rr.Body.String(), customersTestName) || strings.Contains(rr.Body.String(), `"c1"`) { t.Errorf("GET %s with no credential: body names a customer: %q", p, rr.Body.String()) } } } // Scenario B — a box's own keys (per-customer, per-host, another household's) never list customers. func TestCustomersRoutes_BoxKeysAreRefused(t *testing.T) { h, _ := newCustomersHandler(t) for _, key := range []string{"ckey", "hkey", "c2key", "not-a-key"} { for _, p := range customersRoutes { if rr := do(h, http.MethodGet, p, key, ""); rr.Code != http.StatusForbidden { t.Errorf("GET %s with box key %q: status = %d, want 403", p, key, rr.Code) } } } } // Scenario C — the operator key gets what v0.147.0 returned to anyone. func TestCustomersRoutes_GlobalKeyUnchanged(t *testing.T) { h, st := newCustomersHandler(t) rr := do(h, http.MethodGet, "/customers", globalKey, "") if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), customersTestName) { t.Fatalf("GET /customers with global key: status = %d body=%s", rr.Code, rr.Body.String()) } c, err := st.GetCustomer("c1") if err != nil || c == nil { t.Fatalf("GetCustomer: %v %v", c, err) } rr = do(h, http.MethodGet, "/customers/c1", globalKey, "") if rr.Code != http.StatusOK || rr.Body.String() != c.ReportJSON { t.Fatalf("GET /customers/c1 with global key: status = %d, body differs from the stored report", rr.Code) } if rr = do(h, http.MethodGet, "/customers/c1/history", globalKey, ""); rr.Code != http.StatusOK { t.Fatalf("GET /customers/c1/history with global key: status = %d", rr.Code) } }