package integrations import ( "strings" "testing" ) func TestMustRegistryLoadsEmbeddedManifests(t *testing.T) { reg := MustRegistry() list := reg.List() if len(list) != 5 { t.Fatalf("embedded manifests: got %d, want 5", len(list)) } // List is sorted by id. want := []string{"dnsimple", "github", "hubspot", "linear", "stripe"} for i, m := range list { if m.ID != want[i] { t.Errorf("List()[%d] = %q, want %q", i, m.ID, want[i]) } if reg.Get(m.ID) != m { t.Errorf("Get(%q) doesn't return the listed manifest", m.ID) } } if reg.Get("nope") != nil { t.Error("Get(unknown) should be nil") } gh := reg.Get("github") if op := gh.Op("issue.create"); op == nil || op.Access != "write" { t.Fatalf("github issue.create: got %+v", gh.Op("issue.create")) } if gh.Op("bogus") != nil { t.Error("Op(unknown) should be nil") } // The legacy dnsimple op id "list" must NOT be a manifest op — the // sidecar's legacy path depends on it falling through (connect.go). if reg.Get("dnsimple").Op("list") != nil { t.Error(`dnsimple manifest declares op "list" — that shadows the legacy path`) } } func TestEveryWriteOpHasConfirm(t *testing.T) { for _, m := range MustRegistry().List() { for _, op := range m.Ops { if op.Access == "write" && op.Confirm == "" { t.Errorf("%s.%s: write op without confirm", m.ID, op.ID) } } } } // validManifest is a minimal manifest the validator accepts; the invalid // cases below are single mutations of this shape. const validManifest = `{ "schema": 1, "id": "svc", "name": "Svc", "category": "test", "auth": { "kind": "pat", "base": "https://api.svc.example", "authorization": "Bearer" }, "ops": [ { "id": "things.list", "label": "List", "access": "read", "method": "GET", "path": "/v1/{account}/things?limit=10", "args": { "account": { "required": true } } }, { "id": "thing.create", "label": "Create", "access": "write", "method": "POST", "path": "/v1/things", "args": { "title": { "required": true } }, "body_json": { "title": "{title}" }, "confirm": "Create {title}?" } ] }` func TestParseValidManifest(t *testing.T) { m, err := Parse([]byte(validManifest)) if err != nil { t.Fatalf("valid manifest rejected: %v", err) } if m.ID != "svc" || len(m.Ops) != 2 { t.Fatalf("parsed shape wrong: %+v", m) } } func TestParseOAuthScopeLists(t *testing.T) { // scopes / scopes_write are optional public data the broker hands the // client for the authorize URL; parsing must carry both through. raw := strings.Replace(validManifest, `"kind": "pat"`, `"kind": "both", "oauth": { "authorize_url": "https://svc.example/oauth", "token_url": "https://api.svc.example/token", "scopes": ["read"], "scopes_write": ["read", "write"] }`, 1) m, err := Parse([]byte(raw)) if err != nil { t.Fatalf("oauth manifest rejected: %v", err) } o := m.Auth.OAuth if o == nil || len(o.Scopes) != 1 || len(o.ScopesWrite) != 2 || o.ScopesWrite[1] != "write" { t.Fatalf("oauth scopes: %+v", o) } } func TestParseRejectsInvalidManifests(t *testing.T) { // Each case is the valid manifest with one rule broken; every one must be // rejected outright (fail closed — no partial load). mutate := func(old, new string) string { s := strings.Replace(validManifest, old, new, 1) if s == validManifest { t.Fatalf("mutation %q not applied", new) } return s } cases := []struct { name string json string }{ {"bad schema", mutate(`"schema": 1`, `"schema": 2`)}, {"bad manifest id", mutate(`"id": "svc"`, `"id": "Svc!"`)}, {"unknown access", mutate(`"access": "read"`, `"access": "admin"`)}, {"write without confirm", mutate(`"confirm": "Create {title}?"`, `"confirm": ""`)}, {"undeclared placeholder in path", mutate(`"path": "/v1/{account}/things?limit=10"`, `"path": "/v1/{oops}/things"`)}, {"undeclared placeholder in body_json", mutate(`"body_json": { "title": "{title}" }`, `"body_json": { "title": "{oops}" }`)}, {"undeclared placeholder in confirm", mutate(`"confirm": "Create {title}?"`, `"confirm": "Create {oops}?"`)}, {"http base", mutate(`"base": "https://api.svc.example"`, `"base": "http://api.svc.example"`)}, {"base with a path", mutate(`"base": "https://api.svc.example"`, `"base": "https://api.svc.example/v2"`)}, {"base without host", mutate(`"base": "https://api.svc.example"`, `"base": "https://"`)}, {"duplicate op ids", mutate(`"id": "thing.create"`, `"id": "things.list"`)}, {"bad op id", mutate(`"id": "things.list"`, `"id": "Things.List"`)}, {"bad method", mutate(`"method": "GET"`, `"method": "TRACE"`)}, {"form without body_form", mutate(`"body_json": { "title": "{title}" },`, `"encoding": "form",`)}, {"bad encoding", mutate(`"body_json": { "title": "{title}" },`, `"body_json": { "title": "{title}" }, "encoding": "xml",`)}, {"empty pick", mutate(`"path": "/v1/things",`, `"path": "/v1/things", "pick": {},`)}, {"bad pick root", mutate(`"path": "/v1/things",`, `"path": "/v1/things", "pick": { "root": "data..x" },`)}, {"bad pick field", mutate(`"path": "/v1/things",`, `"path": "/v1/things", "pick": { "fields": ["ok", "no spaces"] },`)}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if _, err := Parse([]byte(c.json)); err == nil { t.Errorf("%s: accepted, want rejection", c.name) } }) } } func TestRenderConfirm(t *testing.T) { op := MustRegistry().Get("github").Op("issue.create") got := op.RenderConfirm(map[string]string{"title": "Fix login", "owner": "paul", "repo": "lchat"}) if !strings.Contains(got, "Fix login") || !strings.Contains(got, "paul/lchat") { t.Errorf("RenderConfirm: %q", got) } // A missing arg stays literal, so the chip reads visibly wrong instead of // asking consent for a blank. got = op.RenderConfirm(map[string]string{"owner": "paul"}) if !strings.Contains(got, "{title}") || !strings.Contains(got, "{repo}") { t.Errorf("RenderConfirm with missing args: %q", got) } }