176 lines
5.4 KiB
Go
176 lines
5.4 KiB
Go
package api_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/uopi/uopi/internal/access"
|
|
"github.com/uopi/uopi/internal/api"
|
|
"github.com/uopi/uopi/internal/audit"
|
|
"github.com/uopi/uopi/internal/broker"
|
|
"github.com/uopi/uopi/internal/confmgr"
|
|
"github.com/uopi/uopi/internal/controllogic"
|
|
"github.com/uopi/uopi/internal/datasource/stub"
|
|
"github.com/uopi/uopi/internal/datasource/synthetic"
|
|
"github.com/uopi/uopi/internal/panelacl"
|
|
"github.com/uopi/uopi/internal/storage"
|
|
)
|
|
|
|
// setupSynthetic builds an API server whose synthetic data source is enabled, so
|
|
// the synthetic CRUD/versioning/trace handlers run their real bodies.
|
|
func setupSynthetic(t *testing.T) (*httptest.Server, func()) {
|
|
t.Helper()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
log := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
brk := broker.New(ctx, log)
|
|
ds := stub.New()
|
|
if err := ds.Connect(ctx); err != nil {
|
|
t.Fatal("stub connect:", err)
|
|
}
|
|
brk.Register(ds)
|
|
|
|
dir := t.TempDir()
|
|
store, _ := storage.New(dir)
|
|
acl, _ := panelacl.New(dir)
|
|
clStore, _ := controllogic.NewStore(dir)
|
|
cfgStore, _ := confmgr.New(dir)
|
|
clEngine := controllogic.NewEngine(ctx, brk, clStore, cfgStore, audit.Nop(), log)
|
|
|
|
synthDS := synthetic.New(dir, brk, log)
|
|
if err := synthDS.Connect(ctx); err != nil {
|
|
t.Fatal("synthetic connect:", err)
|
|
}
|
|
brk.Register(synthDS)
|
|
|
|
mux := http.NewServeMux()
|
|
api.New(brk, synthDS, store, cfgStore, access.New("", nil), acl, clStore, clEngine, audit.Nop(), "", "", 0, log).Register(mux, "/api/v1")
|
|
|
|
srv := httptest.NewServer(mux)
|
|
return srv, func() { srv.Close(); cancel() }
|
|
}
|
|
|
|
// putJSON marshals body and issues a PUT, mirroring postJSON.
|
|
func putJSON(t *testing.T, srv *httptest.Server, path string, body any) *http.Response {
|
|
t.Helper()
|
|
b, err := json.Marshal(body)
|
|
if err != nil {
|
|
t.Fatal("json.Marshal:", err)
|
|
}
|
|
return putRaw(t, srv, path, "application/json", b)
|
|
}
|
|
|
|
// syntheticBody returns a minimal valid graph signal sourced from the stub's
|
|
// "sine" signal through a gain op.
|
|
func syntheticBody(name string) map[string]any {
|
|
return map[string]any{
|
|
"name": name,
|
|
"visibility": "global",
|
|
"graph": map[string]any{
|
|
"output": "out",
|
|
"nodes": []map[string]any{
|
|
{"id": "a", "kind": "source", "ds": "stub", "signal": "sine"},
|
|
{"id": "g", "kind": "op", "op": "gain", "inputs": []string{"a"},
|
|
"params": map[string]any{"k": 2.0}},
|
|
{"id": "out", "kind": "output", "inputs": []string{"g"}},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestSyntheticCRUDEnabled(t *testing.T) {
|
|
srv, teardown := setupSynthetic(t)
|
|
defer teardown()
|
|
|
|
// List — initially empty.
|
|
resp := get(t, srv, "/api/v1/synthetic")
|
|
assertStatus(t, resp, http.StatusOK)
|
|
var list []any
|
|
readJSON(t, resp, &list)
|
|
if len(list) != 0 {
|
|
t.Fatalf("expected no synthetic signals, got %d", len(list))
|
|
}
|
|
|
|
// Create.
|
|
resp = postJSON(t, srv, "/api/v1/synthetic", syntheticBody("doubled"))
|
|
assertStatus(t, resp, http.StatusCreated)
|
|
resp.Body.Close()
|
|
|
|
// Duplicate create → 400 (AddSignal rejects an existing name).
|
|
resp = postJSON(t, srv, "/api/v1/synthetic", syntheticBody("doubled"))
|
|
assertStatus(t, resp, http.StatusBadRequest)
|
|
|
|
// Invalid JSON → 400.
|
|
resp = postRaw(t, srv, "/api/v1/synthetic", "application/json", []byte(`{bad`))
|
|
assertStatus(t, resp, http.StatusBadRequest)
|
|
|
|
// Get.
|
|
resp = get(t, srv, "/api/v1/synthetic/doubled")
|
|
assertStatus(t, resp, http.StatusOK)
|
|
resp.Body.Close()
|
|
|
|
// Get missing → 404.
|
|
resp = get(t, srv, "/api/v1/synthetic/nope")
|
|
assertStatus(t, resp, http.StatusNotFound)
|
|
|
|
// Update (changes gain factor).
|
|
upd := syntheticBody("doubled")
|
|
upd["graph"].(map[string]any)["nodes"].([]map[string]any)[1]["params"] = map[string]any{"k": 3.0}
|
|
resp = putJSON(t, srv, "/api/v1/synthetic/doubled", upd)
|
|
assertStatus(t, resp, http.StatusOK)
|
|
resp.Body.Close()
|
|
|
|
// Update missing → 404.
|
|
resp = putJSON(t, srv, "/api/v1/synthetic/nope", syntheticBody("nope"))
|
|
assertStatus(t, resp, http.StatusNotFound)
|
|
|
|
// Versions list (the update created a backup).
|
|
resp = get(t, srv, "/api/v1/synthetic/doubled/versions")
|
|
assertStatus(t, resp, http.StatusOK)
|
|
var versions []map[string]any
|
|
readJSON(t, resp, &versions)
|
|
if len(versions) < 2 {
|
|
t.Fatalf("expected >=2 synthetic versions, got %d", len(versions))
|
|
}
|
|
|
|
// Get v1.
|
|
resp = get(t, srv, "/api/v1/synthetic/doubled/versions/1")
|
|
assertStatus(t, resp, http.StatusOK)
|
|
resp.Body.Close()
|
|
|
|
// Bad version → 400.
|
|
resp = get(t, srv, "/api/v1/synthetic/doubled/versions/xyz")
|
|
assertStatus(t, resp, http.StatusBadRequest)
|
|
|
|
// Promote v1.
|
|
resp = postRaw(t, srv, "/api/v1/synthetic/doubled/versions/1/promote", "application/json", nil)
|
|
assertStatus(t, resp, http.StatusOK)
|
|
resp.Body.Close()
|
|
|
|
// Fork v1 → new signal.
|
|
resp = postRaw(t, srv, "/api/v1/synthetic/doubled/versions/1/fork", "application/json", nil)
|
|
assertStatus(t, resp, http.StatusCreated)
|
|
resp.Body.Close()
|
|
|
|
// Trace an unsaved graph.
|
|
resp = postJSON(t, srv, "/api/v1/synthetic/trace", syntheticBody("scratch"))
|
|
assertStatus(t, resp, http.StatusOK)
|
|
resp.Body.Close()
|
|
|
|
// Trace with invalid JSON → 400.
|
|
resp = postRaw(t, srv, "/api/v1/synthetic/trace", "application/json", []byte(`{bad`))
|
|
assertStatus(t, resp, http.StatusBadRequest)
|
|
|
|
// Delete.
|
|
resp = deleteReq(t, srv, "/api/v1/synthetic/doubled")
|
|
assertStatus(t, resp, http.StatusNoContent)
|
|
|
|
// Delete missing → 404.
|
|
resp = deleteReq(t, srv, "/api/v1/synthetic/doubled")
|
|
assertStatus(t, resp, http.StatusNotFound)
|
|
}
|