Testing
This commit is contained in:
@@ -0,0 +1,438 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// These tests exercise the many handlers reachable through the default setup()
|
||||
// harness, whose policy is unconfigured (anonymous caller → write/admin), so
|
||||
// permission gates default to "allow" and the focus is handler behaviour.
|
||||
|
||||
// ── /me ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestGetMe(t *testing.T) {
|
||||
srv, teardown := setup(t)
|
||||
defer teardown()
|
||||
|
||||
resp := get(t, srv, "/api/v1/me")
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
var me map[string]any
|
||||
readJSON(t, resp, &me)
|
||||
for _, k := range []string{"user", "level", "groups", "canEditLogic", "canViewAudit", "canAdmin", "defaultZoom"} {
|
||||
if _, ok := me[k]; !ok {
|
||||
t.Errorf("/me missing key %q", k)
|
||||
}
|
||||
}
|
||||
// Unconfigured policy → anonymous caller has write level.
|
||||
if me["level"] != "write" {
|
||||
t.Errorf("level = %v, want write", me["level"])
|
||||
}
|
||||
}
|
||||
|
||||
// ── /groups (signal group tree) ───────────────────────────────────────────────
|
||||
|
||||
func TestGroupsRoundTrip(t *testing.T) {
|
||||
srv, teardown := setup(t)
|
||||
defer teardown()
|
||||
|
||||
// Initially valid JSON.
|
||||
resp := get(t, srv, "/api/v1/groups")
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
resp.Body.Close()
|
||||
|
||||
// Replace with a JSON array → 204.
|
||||
resp = putRaw(t, srv, "/api/v1/groups", "application/json", []byte(`[{"name":"A"},{"name":"B"}]`))
|
||||
assertStatus(t, resp, http.StatusNoContent)
|
||||
|
||||
// Read back what we wrote.
|
||||
resp = get(t, srv, "/api/v1/groups")
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
var tree []map[string]any
|
||||
if err := json.Unmarshal(body, &tree); err != nil {
|
||||
t.Fatalf("groups body not a JSON array: %v (%s)", err, body)
|
||||
}
|
||||
if len(tree) != 2 {
|
||||
t.Fatalf("expected 2 groups, got %d", len(tree))
|
||||
}
|
||||
|
||||
// A non-array body is rejected.
|
||||
resp = putRaw(t, srv, "/api/v1/groups", "application/json", []byte(`{"not":"array"}`))
|
||||
assertStatus(t, resp, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// ── /usergroups ───────────────────────────────────────────────────────────────
|
||||
|
||||
func TestListUserGroups(t *testing.T) {
|
||||
srv, teardown := setup(t)
|
||||
defer teardown()
|
||||
|
||||
resp := get(t, srv, "/api/v1/usergroups")
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
var names []string
|
||||
readJSON(t, resp, &names)
|
||||
// Unconfigured policy has no named groups; the handler must still return [].
|
||||
if names == nil {
|
||||
t.Error("expected non-nil (possibly empty) group list")
|
||||
}
|
||||
}
|
||||
|
||||
// ── /folders ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestFolderCRUD(t *testing.T) {
|
||||
srv, teardown := setup(t)
|
||||
defer teardown()
|
||||
|
||||
// List — initially empty.
|
||||
resp := get(t, srv, "/api/v1/folders")
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
var list []any
|
||||
readJSON(t, resp, &list)
|
||||
if len(list) != 0 {
|
||||
t.Fatalf("expected no folders, got %d", len(list))
|
||||
}
|
||||
|
||||
// Missing name → 400.
|
||||
resp = postJSON(t, srv, "/api/v1/folders", map[string]any{"name": ""})
|
||||
assertStatus(t, resp, http.StatusBadRequest)
|
||||
|
||||
// Create.
|
||||
resp = postJSON(t, srv, "/api/v1/folders", map[string]any{"name": "Reactor"})
|
||||
assertStatus(t, resp, http.StatusCreated)
|
||||
var created struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
readJSON(t, resp, &created)
|
||||
if created.ID == "" {
|
||||
t.Fatal("expected folder id")
|
||||
}
|
||||
|
||||
// Update.
|
||||
resp = putRaw(t, srv, "/api/v1/folders/"+created.ID, "application/json",
|
||||
[]byte(`{"name":"Reactor Hall"}`))
|
||||
assertStatus(t, resp, http.StatusNoContent)
|
||||
|
||||
// Update with empty name → 400.
|
||||
resp = putRaw(t, srv, "/api/v1/folders/"+created.ID, "application/json", []byte(`{"name":""}`))
|
||||
assertStatus(t, resp, http.StatusBadRequest)
|
||||
|
||||
// Update unknown folder → 404.
|
||||
resp = putRaw(t, srv, "/api/v1/folders/fld-nope", "application/json", []byte(`{"name":"x"}`))
|
||||
assertStatus(t, resp, http.StatusNotFound)
|
||||
|
||||
// It now appears in the list.
|
||||
resp = get(t, srv, "/api/v1/folders")
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
readJSON(t, resp, &list)
|
||||
if len(list) != 1 {
|
||||
t.Fatalf("expected 1 folder, got %d", len(list))
|
||||
}
|
||||
|
||||
// Delete.
|
||||
resp = deleteReq(t, srv, "/api/v1/folders/"+created.ID)
|
||||
assertStatus(t, resp, http.StatusNoContent)
|
||||
|
||||
// Delete unknown → 404.
|
||||
resp = deleteReq(t, srv, "/api/v1/folders/fld-nope")
|
||||
assertStatus(t, resp, http.StatusNotFound)
|
||||
}
|
||||
|
||||
// ── /interfaces/{id}/acl ──────────────────────────────────────────────────────
|
||||
|
||||
func TestPanelACL(t *testing.T) {
|
||||
srv, teardown := setup(t)
|
||||
defer teardown()
|
||||
|
||||
resp := postRaw(t, srv, "/api/v1/interfaces", "application/xml", []byte(sampleXML))
|
||||
assertStatus(t, resp, http.StatusCreated)
|
||||
var created struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
readJSON(t, resp, &created)
|
||||
|
||||
// GET ACL of a fresh panel.
|
||||
resp = get(t, srv, "/api/v1/interfaces/"+created.ID+"/acl")
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
var acl map[string]any
|
||||
readJSON(t, resp, &acl)
|
||||
if _, ok := acl["perm"]; !ok {
|
||||
t.Error("ACL response missing perm")
|
||||
}
|
||||
|
||||
// PUT ACL — set a public read level.
|
||||
resp = putRaw(t, srv, "/api/v1/interfaces/"+created.ID+"/acl", "application/json",
|
||||
[]byte(`{"public":"read","grants":[]}`))
|
||||
assertStatus(t, resp, http.StatusNoContent)
|
||||
|
||||
// PUT ACL on a non-existent panel → 404.
|
||||
resp = putRaw(t, srv, "/api/v1/interfaces/nope/acl", "application/json",
|
||||
[]byte(`{"public":"read"}`))
|
||||
assertStatus(t, resp, http.StatusNotFound)
|
||||
|
||||
// PUT ACL referencing an unknown folder → 400.
|
||||
resp = putRaw(t, srv, "/api/v1/interfaces/"+created.ID+"/acl", "application/json",
|
||||
[]byte(`{"folder":"fld-nope"}`))
|
||||
assertStatus(t, resp, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// ── /interfaces/reorder ───────────────────────────────────────────────────────
|
||||
|
||||
func TestReorderInterfaces(t *testing.T) {
|
||||
srv, teardown := setup(t)
|
||||
defer teardown()
|
||||
|
||||
mk := func() string {
|
||||
resp := postRaw(t, srv, "/api/v1/interfaces", "application/xml", []byte(sampleXML))
|
||||
assertStatus(t, resp, http.StatusCreated)
|
||||
var c struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
readJSON(t, resp, &c)
|
||||
return c.ID
|
||||
}
|
||||
a, b := mk(), mk()
|
||||
|
||||
// Reorder at root (no folder).
|
||||
resp := postJSON(t, srv, "/api/v1/interfaces/reorder", map[string]any{"ids": []string{b, a}})
|
||||
assertStatus(t, resp, http.StatusNoContent)
|
||||
|
||||
// Reorder into a non-existent folder → 404.
|
||||
resp = postJSON(t, srv, "/api/v1/interfaces/reorder",
|
||||
map[string]any{"folder": "fld-nope", "ids": []string{a}})
|
||||
assertStatus(t, resp, http.StatusNotFound)
|
||||
|
||||
// Malformed body → 400.
|
||||
resp = postRaw(t, srv, "/api/v1/interfaces/reorder", "application/json", []byte(`{not json`))
|
||||
assertStatus(t, resp, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// ── /interfaces/{id}/versions ─────────────────────────────────────────────────
|
||||
|
||||
func TestInterfaceVersioning(t *testing.T) {
|
||||
srv, teardown := setup(t)
|
||||
defer teardown()
|
||||
|
||||
resp := postRaw(t, srv, "/api/v1/interfaces", "application/xml", []byte(sampleXML))
|
||||
assertStatus(t, resp, http.StatusCreated)
|
||||
var created struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
readJSON(t, resp, &created)
|
||||
id := created.ID
|
||||
|
||||
// Update once to create a backup (v1) and bump current to v2.
|
||||
upd := `<interface id="" name="Test Panel" version="2" w="800" h="600"></interface>`
|
||||
resp = putRaw(t, srv, "/api/v1/interfaces/"+id, "application/xml", []byte(upd))
|
||||
assertStatus(t, resp, http.StatusNoContent)
|
||||
|
||||
// List versions — at least the backup.
|
||||
resp = get(t, srv, "/api/v1/interfaces/"+id+"/versions")
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
var versions []map[string]any
|
||||
readJSON(t, resp, &versions)
|
||||
if len(versions) < 2 {
|
||||
t.Fatalf("expected >=2 versions, got %d", len(versions))
|
||||
}
|
||||
|
||||
// Get the v1 backup.
|
||||
resp = get(t, srv, "/api/v1/interfaces/"+id+"/versions/1")
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
resp.Body.Close()
|
||||
|
||||
// Bad version path → 400.
|
||||
resp = get(t, srv, "/api/v1/interfaces/"+id+"/versions/abc")
|
||||
assertStatus(t, resp, http.StatusBadRequest)
|
||||
|
||||
// Tag v1.
|
||||
resp = putRaw(t, srv, "/api/v1/interfaces/"+id+"/versions/1/tag?tag=golden", "application/json", nil)
|
||||
assertStatus(t, resp, http.StatusNoContent)
|
||||
|
||||
// Promote v1 → becomes new current.
|
||||
resp = postRaw(t, srv, "/api/v1/interfaces/"+id+"/versions/1/promote", "application/json", nil)
|
||||
assertStatus(t, resp, http.StatusNoContent)
|
||||
|
||||
// Fork v1 → brand-new panel.
|
||||
resp = postRaw(t, srv, "/api/v1/interfaces/"+id+"/versions/1/fork", "application/json", nil)
|
||||
assertStatus(t, resp, http.StatusCreated)
|
||||
var fork struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
readJSON(t, resp, &fork)
|
||||
if fork.ID == "" || fork.ID == id {
|
||||
t.Errorf("fork id = %q, want fresh id", fork.ID)
|
||||
}
|
||||
|
||||
// Version ops on a missing panel → 404.
|
||||
resp = get(t, srv, "/api/v1/interfaces/missing/versions")
|
||||
assertStatus(t, resp, http.StatusNotFound)
|
||||
}
|
||||
|
||||
// ── /controllogic ─────────────────────────────────────────────────────────────
|
||||
|
||||
func TestControlLogicCRUD(t *testing.T) {
|
||||
srv, teardown := setup(t)
|
||||
defer teardown()
|
||||
|
||||
// Empty list initially.
|
||||
resp := get(t, srv, "/api/v1/controllogic")
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
var list []any
|
||||
readJSON(t, resp, &list)
|
||||
if len(list) != 0 {
|
||||
t.Fatalf("expected no control logic, got %d", len(list))
|
||||
}
|
||||
|
||||
// Create.
|
||||
resp = postJSON(t, srv, "/api/v1/controllogic", map[string]any{"name": "Watchdog", "enabled": true})
|
||||
assertStatus(t, resp, http.StatusCreated)
|
||||
var g struct {
|
||||
ID string `json:"id"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
readJSON(t, resp, &g)
|
||||
if g.ID == "" {
|
||||
t.Fatal("expected control logic id")
|
||||
}
|
||||
|
||||
// Get.
|
||||
resp = get(t, srv, "/api/v1/controllogic/"+g.ID)
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
resp.Body.Close()
|
||||
|
||||
// Get missing → 404.
|
||||
resp = get(t, srv, "/api/v1/controllogic/nope")
|
||||
assertStatus(t, resp, http.StatusNotFound)
|
||||
|
||||
// Update (bumps version, creates a backup).
|
||||
resp = putRaw(t, srv, "/api/v1/controllogic/"+g.ID, "application/json",
|
||||
[]byte(`{"name":"Watchdog v2","enabled":false}`))
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
|
||||
// List versions.
|
||||
resp = get(t, srv, "/api/v1/controllogic/"+g.ID+"/versions")
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
var versions []map[string]any
|
||||
readJSON(t, resp, &versions)
|
||||
if len(versions) < 2 {
|
||||
t.Fatalf("expected >=2 control-logic versions, got %d", len(versions))
|
||||
}
|
||||
|
||||
// Promote v1.
|
||||
resp = postRaw(t, srv, "/api/v1/controllogic/"+g.ID+"/versions/1/promote", "application/json", nil)
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
|
||||
// Fork v1 → new graph id.
|
||||
resp = postRaw(t, srv, "/api/v1/controllogic/"+g.ID+"/versions/1/fork", "application/json", nil)
|
||||
assertStatus(t, resp, http.StatusCreated)
|
||||
var fork struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
readJSON(t, resp, &fork)
|
||||
if fork.ID == "" || fork.ID == g.ID {
|
||||
t.Errorf("fork id = %q, want fresh id", fork.ID)
|
||||
}
|
||||
|
||||
// Delete.
|
||||
resp = deleteReq(t, srv, "/api/v1/controllogic/"+g.ID)
|
||||
assertStatus(t, resp, http.StatusNoContent)
|
||||
|
||||
// Delete missing → 404.
|
||||
resp = deleteReq(t, srv, "/api/v1/controllogic/"+g.ID)
|
||||
assertStatus(t, resp, http.StatusNotFound)
|
||||
}
|
||||
|
||||
// ── /audit (Nop audit log, anonymous can view on an open policy) ───────────────
|
||||
|
||||
func TestGetAuditOpen(t *testing.T) {
|
||||
srv, teardown := setup(t)
|
||||
defer teardown()
|
||||
|
||||
resp := get(t, srv, "/api/v1/audit")
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
resp.Body.Close()
|
||||
|
||||
// Bad start time → 400.
|
||||
resp = get(t, srv, "/api/v1/audit?start=not-a-time")
|
||||
assertStatus(t, resp, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// ── /synthetic disabled guards ────────────────────────────────────────────────
|
||||
|
||||
// Synthetic is nil in the default harness, so every route must report 503
|
||||
// (service unavailable) rather than panicking.
|
||||
func TestSyntheticDisabledRoutes(t *testing.T) {
|
||||
srv, teardown := setup(t)
|
||||
defer teardown()
|
||||
|
||||
cases := []struct {
|
||||
method, path string
|
||||
body []byte
|
||||
}{
|
||||
{http.MethodGet, "/api/v1/synthetic/x", nil},
|
||||
{http.MethodPut, "/api/v1/synthetic/x", []byte(`{}`)},
|
||||
{http.MethodDelete, "/api/v1/synthetic/x", nil},
|
||||
{http.MethodPost, "/api/v1/synthetic/trace", []byte(`{}`)},
|
||||
{http.MethodGet, "/api/v1/synthetic/x/versions", nil},
|
||||
{http.MethodGet, "/api/v1/synthetic/x/versions/1", nil},
|
||||
{http.MethodPost, "/api/v1/synthetic/x/versions/1/promote", nil},
|
||||
{http.MethodPost, "/api/v1/synthetic/x/versions/1/fork", nil},
|
||||
}
|
||||
for _, c := range cases {
|
||||
var resp *http.Response
|
||||
switch c.method {
|
||||
case http.MethodGet:
|
||||
resp = get(t, srv, c.path)
|
||||
case http.MethodPut:
|
||||
resp = putRaw(t, srv, c.path, "application/json", c.body)
|
||||
case http.MethodDelete:
|
||||
resp = deleteReq(t, srv, c.path)
|
||||
case http.MethodPost:
|
||||
resp = postRaw(t, srv, c.path, "application/json", c.body)
|
||||
}
|
||||
if resp.StatusCode != http.StatusServiceUnavailable {
|
||||
t.Errorf("%s %s: status %d, want 503", c.method, c.path, resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// ── /controllogic/{id}/versions/{version} ─────────────────────────────────────
|
||||
|
||||
func TestControlLogicGetVersion(t *testing.T) {
|
||||
srv, teardown := setup(t)
|
||||
defer teardown()
|
||||
|
||||
resp := postJSON(t, srv, "/api/v1/controllogic", map[string]any{"name": "G"})
|
||||
assertStatus(t, resp, http.StatusCreated)
|
||||
var g struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
readJSON(t, resp, &g)
|
||||
|
||||
// Bump so a v1 backup exists.
|
||||
resp = putRaw(t, srv, "/api/v1/controllogic/"+g.ID, "application/json", []byte(`{"name":"G2"}`))
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
|
||||
resp = get(t, srv, "/api/v1/controllogic/"+g.ID+"/versions/1")
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
resp.Body.Close()
|
||||
|
||||
// Bad version number → 400.
|
||||
resp = get(t, srv, "/api/v1/controllogic/"+g.ID+"/versions/xyz")
|
||||
assertStatus(t, resp, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// ── /config/instances/diff ────────────────────────────────────────────────────
|
||||
|
||||
func TestDiffConfigInstancesMissing(t *testing.T) {
|
||||
srv, teardown := setup(t)
|
||||
defer teardown()
|
||||
|
||||
// Missing left id → 400.
|
||||
resp := get(t, srv, "/api/v1/config/instances/diff?a=&b=")
|
||||
assertStatus(t, resp, http.StatusBadRequest)
|
||||
}
|
||||
Reference in New Issue
Block a user