eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1package integrations
2
3import (
4 "strings"
5 "testing"
6)
7
8func TestMustRegistryLoadsEmbeddedManifests(t *testing.T) {
9 reg := MustRegistry()
10 list := reg.List()
11 if len(list) != 5 {
12 t.Fatalf("embedded manifests: got %d, want 5", len(list))
13 }
14 // List is sorted by id.
15 want := []string{"dnsimple", "github", "hubspot", "linear", "stripe"}
16 for i, m := range list {
17 if m.ID != want[i] {
18 t.Errorf("List()[%d] = %q, want %q", i, m.ID, want[i])
19 }
20 if reg.Get(m.ID) != m {
21 t.Errorf("Get(%q) doesn't return the listed manifest", m.ID)
22 }
23 }
24 if reg.Get("nope") != nil {
25 t.Error("Get(unknown) should be nil")
26 }
27 gh := reg.Get("github")
28 if op := gh.Op("issue.create"); op == nil || op.Access != "write" {
29 t.Fatalf("github issue.create: got %+v", gh.Op("issue.create"))
30 }
31 if gh.Op("bogus") != nil {
32 t.Error("Op(unknown) should be nil")
33 }
34 // The legacy dnsimple op id "list" must NOT be a manifest op — the
35 // sidecar's legacy path depends on it falling through (connect.go).
36 if reg.Get("dnsimple").Op("list") != nil {
37 t.Error(`dnsimple manifest declares op "list" — that shadows the legacy path`)
38 }
39}
40
41func TestEveryWriteOpHasConfirm(t *testing.T) {
42 for _, m := range MustRegistry().List() {
43 for _, op := range m.Ops {
44 if op.Access == "write" && op.Confirm == "" {
45 t.Errorf("%s.%s: write op without confirm", m.ID, op.ID)
46 }
47 }
48 }
49}
50
51// validManifest is a minimal manifest the validator accepts; the invalid
52// cases below are single mutations of this shape.
53const validManifest = `{
54 "schema": 1,
55 "id": "svc",
56 "name": "Svc",
57 "category": "test",
58 "auth": { "kind": "pat", "base": "https://api.svc.example", "authorization": "Bearer" },
59 "ops": [
60 { "id": "things.list", "label": "List", "access": "read", "method": "GET",
61 "path": "/v1/{account}/things?limit=10",
62 "args": { "account": { "required": true } } },
63 { "id": "thing.create", "label": "Create", "access": "write", "method": "POST",
64 "path": "/v1/things",
65 "args": { "title": { "required": true } },
66 "body_json": { "title": "{title}" },
67 "confirm": "Create {title}?" }
68 ]
69}`
70
71func TestParseValidManifest(t *testing.T) {
72 m, err := Parse([]byte(validManifest))
73 if err != nil {
74 t.Fatalf("valid manifest rejected: %v", err)
75 }
76 if m.ID != "svc" || len(m.Ops) != 2 {
77 t.Fatalf("parsed shape wrong: %+v", m)
78 }
79}
80
81func TestParseOAuthScopeLists(t *testing.T) {
82 // scopes / scopes_write are optional public data the broker hands the
83 // client for the authorize URL; parsing must carry both through.
84 raw := strings.Replace(validManifest, `"kind": "pat"`,
85 `"kind": "both", "oauth": { "authorize_url": "https://svc.example/oauth", "token_url": "https://api.svc.example/token", "scopes": ["read"], "scopes_write": ["read", "write"] }`, 1)
86 m, err := Parse([]byte(raw))
87 if err != nil {
88 t.Fatalf("oauth manifest rejected: %v", err)
89 }
90 o := m.Auth.OAuth
91 if o == nil || len(o.Scopes) != 1 || len(o.ScopesWrite) != 2 || o.ScopesWrite[1] != "write" {
92 t.Fatalf("oauth scopes: %+v", o)
93 }
94}
95
96func TestParseRejectsInvalidManifests(t *testing.T) {
97 // Each case is the valid manifest with one rule broken; every one must be
98 // rejected outright (fail closed — no partial load).
99 mutate := func(old, new string) string {
100 s := strings.Replace(validManifest, old, new, 1)
101 if s == validManifest {
102 t.Fatalf("mutation %q not applied", new)
103 }
104 return s
105 }
106 cases := []struct {
107 name string
108 json string
109 }{
110 {"bad schema", mutate(`"schema": 1`, `"schema": 2`)},
111 {"bad manifest id", mutate(`"id": "svc"`, `"id": "Svc!"`)},
112 {"unknown access", mutate(`"access": "read"`, `"access": "admin"`)},
113 {"write without confirm", mutate(`"confirm": "Create {title}?"`, `"confirm": ""`)},
114 {"undeclared placeholder in path", mutate(`"path": "/v1/{account}/things?limit=10"`, `"path": "/v1/{oops}/things"`)},
115 {"undeclared placeholder in body_json", mutate(`"body_json": { "title": "{title}" }`, `"body_json": { "title": "{oops}" }`)},
116 {"undeclared placeholder in confirm", mutate(`"confirm": "Create {title}?"`, `"confirm": "Create {oops}?"`)},
117 {"http base", mutate(`"base": "https://api.svc.example"`, `"base": "http://api.svc.example"`)},
118 {"base with a path", mutate(`"base": "https://api.svc.example"`, `"base": "https://api.svc.example/v2"`)},
119 {"base without host", mutate(`"base": "https://api.svc.example"`, `"base": "https://"`)},
120 {"duplicate op ids", mutate(`"id": "thing.create"`, `"id": "things.list"`)},
121 {"bad op id", mutate(`"id": "things.list"`, `"id": "Things.List"`)},
122 {"bad method", mutate(`"method": "GET"`, `"method": "TRACE"`)},
123 {"form without body_form", mutate(`"body_json": { "title": "{title}" },`, `"encoding": "form",`)},
124 {"bad encoding", mutate(`"body_json": { "title": "{title}" },`, `"body_json": { "title": "{title}" }, "encoding": "xml",`)},
125 {"empty pick", mutate(`"path": "/v1/things",`, `"path": "/v1/things", "pick": {},`)},
126 {"bad pick root", mutate(`"path": "/v1/things",`, `"path": "/v1/things", "pick": { "root": "data..x" },`)},
127 {"bad pick field", mutate(`"path": "/v1/things",`, `"path": "/v1/things", "pick": { "fields": ["ok", "no spaces"] },`)},
128 }
129 for _, c := range cases {
130 t.Run(c.name, func(t *testing.T) {
131 if _, err := Parse([]byte(c.json)); err == nil {
132 t.Errorf("%s: accepted, want rejection", c.name)
133 }
134 })
135 }
136}
137
138func TestRenderConfirm(t *testing.T) {
139 op := MustRegistry().Get("github").Op("issue.create")
140 got := op.RenderConfirm(map[string]string{"title": "Fix login", "owner": "paul", "repo": "lchat"})
141 if !strings.Contains(got, "Fix login") || !strings.Contains(got, "paul/lchat") {
142 t.Errorf("RenderConfirm: %q", got)
143 }
144 // A missing arg stays literal, so the chip reads visibly wrong instead of
145 // asking consent for a blank.
146 got = op.RenderConfirm(map[string]string{"owner": "paul"})
147 if !strings.Contains(got, "{title}") || !strings.Contains(got, "{repo}") {
148 t.Errorf("RenderConfirm with missing args: %q", got)
149 }
150}
151