1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242 | (ns ird.coverage-test
"Executable full-coverage inventory: one row per API operation. Each row
invokes the client fn against a capturing transport and asserts the exact
HTTP method and URL, and the set of rows is compared with every operation
the vendored spec declares — so a renamed path, a missing fn, or an
endpoint the API gained fails loudly."
(:require [cljs.test :refer [deftest is]]
[cljs.core.async :refer [<!]]
[clojure.set :as set]
[ird.auth :as auth]
[ird.client :as ird]
[ird.credits :as credits]
[ird.dns :as dns]
[ird.domains :as domains]
[ird.ipfs :as ipfs]
[ird.jobs :as jobs]
[ird.p2p :as p2p]
[ird.pins :as pins]
[ird.registrations :as reg]
[ird.sharing :as sharing]
[ird.support :as support]
[ird.test-util :as tu :refer [deftest-async]]
[ird.transfers :as transfers]
[ird.tunnels :as tunnels]
[ird.vps :as vps]))
(def B "https://api.ird.ro")
(def operations
"[description fn method expected-url args...] — one row per operation,
grouped by namespace. Descriptions are the spec's own \"METHOD /path\"
keys, template placeholders included."
[;; auth + api-keys (6)
["POST /auth/device/start" auth/device-start :post (str B "/auth/device/start")]
["POST /auth/device/poll" auth/device-poll :post (str B "/auth/device/poll") "dc"]
["POST /auth/device/complete" auth/device-complete :post (str B "/auth/device/complete") "dc" "cc"]
["GET /auth/api-keys" auth/list-api-keys :get (str B "/auth/api-keys")]
["POST /auth/api-keys" auth/create-api-key :post (str B "/auth/api-keys") {:name "ci"}]
["POST /auth/api-keys/{id}/revoke" auth/revoke-api-key :post (str B "/auth/api-keys/ak1/revoke") "ak1"]
;; credits (3)
["GET /credits/balance" credits/balance :get (str B "/credits/balance")]
["GET /credits/ledger" credits/ledger :get (str B "/credits/ledger")]
["GET /credits/prices" credits/prices :get (str B "/credits/prices")]
;; support (4)
["GET /contact/threads" support/list-threads :get (str B "/contact/threads")]
["POST /contact/threads" support/create-thread :post (str B "/contact/threads") {:title "t" :service "general" :message "m"}]
["GET /contact/threads/{id}" support/get-thread :get (str B "/contact/threads/t1") "t1"]
["POST /contact/threads/{id}/messages" support/reply :post (str B "/contact/threads/t1/messages") "t1" {:message "m"}]
;; domains + directory (9)
["GET /domains/check" domains/check :get (str B "/domains/check?domain=x.ro") "x.ro"]
["GET /domains/info" domains/info :get (str B "/domains/info?domain=x.ro") "x.ro"]
["GET /domains/preflight" domains/preflight :get (str B "/domains/preflight?domain=x.ro") "x.ro"]
["GET /domains/dns-snapshot" domains/dns-snapshot :get (str B "/domains/dns-snapshot?domain=x.ro") "x.ro"]
["GET /domains/search" domains/search :get (str B "/domains/search?q=shop") "shop"]
["POST /domains/search" domains/search-descriptions :post (str B "/domains/search") {:q "an online shop"}]
["GET /domains/example" domains/example :get (str B "/domains/example")]
["GET /domains/directory" domains/directory :get (str B "/domains/directory")]
["GET /domains/directory/{domain}" domains/directory-entry :get (str B "/domains/directory/x.ro") "x.ro"]
;; registrations (27)
["GET /domains/registrations" reg/list-registrations :get (str B "/domains/registrations")]
["POST /domains/registrations" reg/register :post (str B "/domains/registrations") {:domain "x.ro"}]
["GET /domains/registrations/{id}" reg/get-registration :get (str B "/domains/registrations/r1") "r1"]
["POST /domains/registrations/{id}/renew" reg/renew :post (str B "/domains/registrations/r1/renew") "r1"]
["PUT /domains/registrations/{id}/auto-renew" reg/set-auto-renew :put (str B "/domains/registrations/r1/auto-renew") "r1" {:enabled true}]
["PUT /domains/registrations/{id}/sensitive" reg/set-sensitive :put (str B "/domains/registrations/r1/sensitive") "r1" {:enabled true}]
["PUT /domains/registrations/{id}/nameservers" reg/set-nameservers :put (str B "/domains/registrations/r1/nameservers") "r1" {:nameservers ["ns1.x.ro"]}]
["GET /domains/registrations/{id}/hosts" reg/list-hosts :get (str B "/domains/registrations/r1/hosts") "r1"]
["POST /domains/registrations/{id}/hosts" reg/create-host :post (str B "/domains/registrations/r1/hosts") "r1" {:host "ns1" :ips ["1.2.3.4"]}]
["PUT /domains/registrations/{id}/hosts/{host}" reg/set-host-ips :put (str B "/domains/registrations/r1/hosts/ns1.x.ro") "r1" "ns1.x.ro" {:ips ["1.2.3.4"]}]
["DELETE /domains/registrations/{id}/hosts/{host}" reg/delete-host :delete (str B "/domains/registrations/r1/hosts/ns1.x.ro") "r1" "ns1.x.ro"]
["GET /domains/registrations/{id}/dsdata" reg/list-dsdata :get (str B "/domains/registrations/r1/dsdata") "r1"]
["POST /domains/registrations/{id}/dsdata" reg/publish-dsdata :post (str B "/domains/registrations/r1/dsdata") "r1" {:keytag 1 :alg 13 :digest_type 2 :digest "ab"}]
["DELETE /domains/registrations/{id}/dsdata" reg/withdraw-dsdata :delete (str B "/domains/registrations/r1/dsdata") "r1" {:keytag 1 :alg 13 :digest_type 2 :digest "ab"}]
["POST /domains/registrations/{id}/transfer-out" reg/transfer-out :post (str B "/domains/registrations/r1/transfer-out") "r1"]
["PUT /domains/registrations/{id}/registrant" reg/update-registrant :put (str B "/domains/registrations/r1/registrant") "r1" {:email "a@b.ro"}]
["POST /domains/registrations/{id}/registrant-email/start" reg/start-registrant-email-verification :post (str B "/domains/registrations/r1/registrant-email/start") "r1"]
["POST /domains/registrations/{id}/registrant-email/confirm" reg/confirm-registrant-email :post (str B "/domains/registrations/r1/registrant-email/confirm") "r1" {:token "t" :code "c"}]
["POST /domains/registrations/{id}/registrant-email/adopt" reg/adopt-registrant-email :post (str B "/domains/registrations/r1/registrant-email/adopt") "r1" {:email "a@b.ro"}]
["GET /domains/registrations/{id}/trade" reg/quote-trade :get (str B "/domains/registrations/r1/trade") "r1"]
["POST /domains/registrations/{id}/trade" reg/trade :post (str B "/domains/registrations/r1/trade") "r1" {:auth_key "k"}]
["POST /domains/transfers-in" reg/transfer-in :post (str B "/domains/transfers-in") {:domain "x.ro" :auth_key "k"}]
["POST /domains/trades-in" reg/trade-in :post (str B "/domains/trades-in") {:domain "x.ro" :auth_key "k"}]
["POST /domains/adoptions" reg/adopt :post (str B "/domains/adoptions") {:domain "x.ro"}]
["GET /domains/actions" reg/list-actions :get (str B "/domains/actions")]
["GET /domains/actions/{id}" reg/get-action :get (str B "/domains/actions/a1") "a1"]
["POST /domains/actions/{id}/cancel" reg/cancel-action :post (str B "/domains/actions/a1/cancel") "a1"]
;; dns (18)
["POST /domains/registrations/{id}/dns" dns/enable-hosting :post (str B "/domains/registrations/r1/dns") "r1"]
["DELETE /domains/registrations/{id}/dns" dns/disable-hosting :delete (str B "/domains/registrations/r1/dns") "r1"]
["GET /domains/registrations/{id}/dns/dnssec" dns/get-dnssec :get (str B "/domains/registrations/r1/dns/dnssec") "r1"]
["POST /domains/registrations/{id}/dns/dnssec" dns/enable-dnssec :post (str B "/domains/registrations/r1/dns/dnssec") "r1"]
["DELETE /domains/registrations/{id}/dns/dnssec" dns/disable-dnssec :delete (str B "/domains/registrations/r1/dns/dnssec") "r1"]
["GET /domains/registrations/{id}/dns/nameservers" dns/hosted-nameservers :get (str B "/domains/registrations/r1/dns/nameservers") "r1"]
["POST /domains/registrations/{id}/dns/nameservers" dns/set-hosted-nameservers :post (str B "/domains/registrations/r1/dns/nameservers") "r1" {:mode "labels"}]
["GET /dns/domains" dns/list-zones :get (str B "/dns/domains")]
["GET /dns/domains/{domain}" dns/get-zone :get (str B "/dns/domains/x.ro") "x.ro"]
["PUT /dns/domains/{domain}/records" dns/set-record :put (str B "/dns/domains/x.ro/records") "x.ro" {:record {:name "www" :type "A" :value "1.2.3.4"}}]
["DELETE /dns/domains/{domain}/records" dns/delete-record :delete (str B "/dns/domains/x.ro/records") "x.ro" {:name "www" :type "A"}]
["GET /dns/claims" dns/list-claims :get (str B "/dns/claims")]
["POST /dns/claims" dns/create-claim :post (str B "/dns/claims") {:domain "example.com"}]
["GET /dns/claims/{id}" dns/get-claim :get (str B "/dns/claims/c1") "c1"]
["DELETE /dns/claims/{id}" dns/release-claim :delete (str B "/dns/claims/c1") "c1"]
["PUT /dns/claims/{id}/seed" dns/seed-claim :put (str B "/dns/claims/c1/seed") "c1" {:snapshot_id "s1"}]
["DELETE /dns/claims/{id}/seed" dns/unseed-claim :delete (str B "/dns/claims/c1/seed") "c1"]
["POST /dns/claims/{id}/verify" dns/verify-claim :post (str B "/dns/claims/c1/verify") "c1"]
;; sharing (7)
["GET /domains/registrations/{id}/grants" sharing/list-domain-grants :get (str B "/domains/registrations/r1/grants") "r1"]
["POST /domains/registrations/{id}/grants" sharing/invite :post (str B "/domains/registrations/r1/grants") "r1" {:email "a@b.ro" :scopes ["dns"]}]
["DELETE /domains/registrations/{id}/grants/{grant-id}" sharing/revoke-grant :delete (str B "/domains/registrations/r1/grants/g1") "r1" "g1"]
["GET /grants" sharing/list-grants :get (str B "/grants")]
["POST /grants/{id}/accept" sharing/accept-grant :post (str B "/grants/g1/accept") "g1"]
["POST /grants/{id}/decline" sharing/decline-grant :post (str B "/grants/g1/decline") "g1"]
["POST /grants/{id}/leave" sharing/leave-grant :post (str B "/grants/g1/leave") "g1"]
;; vps (34)
["GET /vps/tiers" vps/list-tiers :get (str B "/vps/tiers")]
["GET /vps/system-images" vps/list-system-images :get (str B "/vps/system-images")]
["GET /vps/ssh-keys" vps/list-ssh-keys :get (str B "/vps/ssh-keys")]
["POST /vps/ssh-keys" vps/add-ssh-key :post (str B "/vps/ssh-keys") {:name "k" :public_key "ssh-ed25519 AAA"}]
["DELETE /vps/ssh-keys/{id}" vps/delete-ssh-key :delete (str B "/vps/ssh-keys/k1") "k1"]
["GET /vps/availability" vps/availability :get (str B "/vps/availability")]
["GET /vps/instances" vps/list-instances :get (str B "/vps/instances")]
["POST /vps/instances" vps/create-instance :post (str B "/vps/instances") {:tier_id "t1" :image_ref "ubuntu"}]
["GET /vps/apps" vps/app-catalog :get (str B "/vps/apps")]
["GET /vps/apps/deployments" vps/list-app-deployments :get (str B "/vps/apps/deployments")]
["POST /vps/apps/deployments" vps/deploy-app :post (str B "/vps/apps/deployments") {:app_id "a1" :tier_id "t1"}]
["GET /vps/apps/deployments/{id}" vps/get-app-deployment :get (str B "/vps/apps/deployments/d1") "d1"]
["POST /vps/apps/deployments/{id}/reveal-password" vps/reveal-app-password :post (str B "/vps/apps/deployments/d1/reveal-password") "d1"]
["GET /vps/instances/{id}" vps/get-instance :get (str B "/vps/instances/i1") "i1"]
["DELETE /vps/instances/{id}" vps/destroy-instance :delete (str B "/vps/instances/i1") "i1"]
["GET /vps/instances/{id}/metrics" vps/get-metrics :get (str B "/vps/instances/i1/metrics") "i1"]
["POST /vps/instances/{id}/queue-notify" vps/set-queue-notify :post (str B "/vps/instances/i1/queue-notify") "i1" {:enabled true}]
["POST /vps/instances/{id}/images" vps/create-snapshot :post (str B "/vps/instances/i1/images") "i1"]
["POST /vps/instances/{id}/restart" vps/restart :post (str B "/vps/instances/i1/restart") "i1"]
["POST /vps/instances/{id}/reset" vps/reset :post (str B "/vps/instances/i1/reset") "i1"]
["POST /vps/instances/{id}/shutdown" vps/shutdown :post (str B "/vps/instances/i1/shutdown") "i1"]
["POST /vps/instances/{id}/power-off" vps/power-off :post (str B "/vps/instances/i1/power-off") "i1"]
["POST /vps/instances/{id}/power-on" vps/power-on :post (str B "/vps/instances/i1/power-on") "i1"]
["PUT /vps/instances/{id}/rdns" vps/set-rdns :put (str B "/vps/instances/i1/rdns") "i1" {:hostname "srv.x.ro"}]
["GET /vps/instances/{id}/domains" vps/list-instance-domains :get (str B "/vps/instances/i1/domains") "i1"]
["POST /vps/instances/{id}/domains" vps/connect-domain :post (str B "/vps/instances/i1/domains") "i1" {:zone "x.ro" :subdomain "www"}]
["DELETE /vps/instances/{id}/domains/{connection-id}" vps/disconnect-domain :delete (str B "/vps/instances/i1/domains/c1") "i1" "c1"]
["GET /vps/volumes" vps/list-volumes :get (str B "/vps/volumes")]
["POST /vps/volumes" vps/create-volume :post (str B "/vps/volumes") {:name "v" :size_gb 10}]
["DELETE /vps/volumes/{id}" vps/delete-volume :delete (str B "/vps/volumes/v1") "v1"]
["GET /vps/images" vps/list-images :get (str B "/vps/images")]
["DELETE /vps/images/{id}" vps/delete-image :delete (str B "/vps/images/img1") "img1"]
["GET /vps/actions/{id}" vps/get-action :get (str B "/vps/actions/a1") "a1"]
["GET /vps/activity" vps/list-activity :get (str B "/vps/activity")]
;; transfers (9)
["POST /vps/instances/{id}/transfer" transfers/offer-instance :post (str B "/vps/instances/i1/transfer") "i1" {:email "a@b.ro"}]
["DELETE /vps/instances/{id}/transfer" transfers/withdraw-instance-offer :delete (str B "/vps/instances/i1/transfer") "i1"]
["POST /vps/volumes/{id}/transfer" transfers/offer-volume :post (str B "/vps/volumes/v1/transfer") "v1" {:email "a@b.ro"}]
["DELETE /vps/volumes/{id}/transfer" transfers/withdraw-volume-offer :delete (str B "/vps/volumes/v1/transfer") "v1"]
["POST /vps/images/{id}/transfer" transfers/offer-image :post (str B "/vps/images/img1/transfer") "img1" {:email "a@b.ro"}]
["DELETE /vps/images/{id}/transfer" transfers/withdraw-image-offer :delete (str B "/vps/images/img1/transfer") "img1"]
["GET /vps/transfers" transfers/list-transfers :get (str B "/vps/transfers")]
["POST /vps/transfers/{id}/accept" transfers/accept-transfer :post (str B "/vps/transfers/tr1/accept") "tr1"]
["POST /vps/transfers/{id}/decline" transfers/decline-transfer :post (str B "/vps/transfers/tr1/decline") "tr1"]
;; tunnels (6)
["GET /tunnels" tunnels/list-tunnels :get (str B "/tunnels")]
["POST /tunnels" tunnels/create-tunnel :post (str B "/tunnels") {:hostname "dev.x.ro"}]
["GET /tunnels/{id}" tunnels/get-tunnel :get (str B "/tunnels/tn1") "tn1"]
["DELETE /tunnels/{id}" tunnels/delete-tunnel :delete (str B "/tunnels/tn1") "tn1"]
["PATCH /tunnels/{id}" tunnels/update-tunnel :patch (str B "/tunnels/tn1") "tn1" {:settings {}}]
["POST /tunnels/{id}/verify-dns" tunnels/verify-dns :post (str B "/tunnels/tn1/verify-dns") "tn1"]
;; ipfs (18)
["GET /ipfs/pins" ipfs/list-pins :get (str B "/ipfs/pins")]
["POST /ipfs/pins" ipfs/pin :post (str B "/ipfs/pins") {:cid "bafy1"}]
["GET /ipfs/pins/{id}" ipfs/get-pin :get (str B "/ipfs/pins/p1") "p1"]
["DELETE /ipfs/pins/{id}" ipfs/unpin :delete (str B "/ipfs/pins/p1") "p1"]
["POST /ipfs/uploads" ipfs/start-upload :post (str B "/ipfs/uploads")]
["GET /ipfs/uploads/{id}" ipfs/get-upload :get (str B "/ipfs/uploads/u1") "u1"]
["GET /ipfs/names" ipfs/list-names :get (str B "/ipfs/names")]
["POST /ipfs/names" ipfs/create-name :post (str B "/ipfs/names")]
["GET /ipfs/names/{id}" ipfs/get-name :get (str B "/ipfs/names/n1") "n1"]
["DELETE /ipfs/names/{id}" ipfs/delete-name :delete (str B "/ipfs/names/n1") "n1"]
["PUT /ipfs/names/{id}/target" ipfs/publish-target :put (str B "/ipfs/names/n1/target") "n1" {:cid "bafy1"}]
["GET /ipfs/domains" ipfs/list-domains :get (str B "/ipfs/domains")]
["POST /ipfs/domains" ipfs/attach-domain :post (str B "/ipfs/domains") {:domain "gw.x.ro"}]
["GET /ipfs/domains/{id}" ipfs/get-domain :get (str B "/ipfs/domains/d1") "d1"]
["GET /ipfs/domains/{id}/pins" ipfs/list-domain-pins :get (str B "/ipfs/domains/d1/pins") "d1"]
["POST /ipfs/domains/{id}/pins" ipfs/update-domain-pins :post (str B "/ipfs/domains/d1/pins") "d1" {:add ["p1"]}]
["DELETE /ipfs/domains/{id}" ipfs/detach-domain :delete (str B "/ipfs/domains/d1") "d1"]
["GET /ipfs/usage" ipfs/usage :get (str B "/ipfs/usage")]
;; pins — Pinning Service API (5)
["GET /pins" pins/list-pins :get (str B "/pins")]
["POST /pins" pins/add-pin :post (str B "/pins") {:cid "bafy1"}]
["GET /pins/{requestid}" pins/get-pin :get (str B "/pins/rq1") "rq1"]
["POST /pins/{requestid}" pins/replace-pin :post (str B "/pins/rq1") "rq1" {:cid "bafy2"}]
["DELETE /pins/{requestid}" pins/remove-pin :delete (str B "/pins/rq1") "rq1"]
;; p2p (12)
["GET /p2p/apps" p2p/list-apps :get (str B "/p2p/apps")]
["POST /p2p/apps" p2p/create-app :post (str B "/p2p/apps") {:name "app"}]
["GET /p2p/apps/{id}" p2p/get-app :get (str B "/p2p/apps/a1") "a1"]
["DELETE /p2p/apps/{id}" p2p/delete-app :delete (str B "/p2p/apps/a1") "a1"]
["PATCH /p2p/apps/{id}" p2p/update-app :patch (str B "/p2p/apps/a1") "a1" {:origins ["https://x.ro"]}]
["POST /p2p/apps/{id}/turn-credentials" p2p/mint-turn-credentials :post (str B "/p2p/apps/a1/turn-credentials") "a1"]
["POST /p2p/apps/{id}/mailbox-credentials" p2p/mint-mailbox-credentials :post (str B "/p2p/apps/a1/mailbox-credentials") "a1"]
["GET /p2p/apps/{id}/domains" p2p/list-app-domains :get (str B "/p2p/apps/a1/domains") "a1"]
["POST /p2p/apps/{id}/domains" p2p/attach-domain :post (str B "/p2p/apps/a1/domains") "a1" {:hostname "p2p.x.ro"}]
["DELETE /p2p/apps/{id}/domains/{domain-id}" p2p/detach-domain :delete (str B "/p2p/apps/a1/domains/d1") "a1" "d1"]
["POST /p2p/apps/{id}/domains/{domain-id}/verify-dns" p2p/verify-domain-dns :post (str B "/p2p/apps/a1/domains/d1/verify-dns") "a1" "d1"]
["GET /p2p/usage" p2p/usage :get (str B "/p2p/usage")]
;; jobs (3)
["POST /jobs" jobs/enqueue :post (str B "/jobs") {:type "embedding" :input "x"}]
["GET /jobs/{id}" jobs/get-job :get (str B "/jobs/j1") "j1"]
["GET /jobs/{id}/result" jobs/get-result :get (str B "/jobs/j1/result") "j1"]])
(deftest inventory-equals-the-spec
(let [described (set (map first operations))
declared (tu/spec-operations)]
(is (= (count operations) (count described)) "no duplicate rows")
(is (= 161 (count declared)) "spec operation count (update README and site when this moves)")
(is (= declared described)
(str "\n in spec but not in table: " (pr-str (sort (set/difference declared described)))
"\n in table but not in spec: " (pr-str (sort (set/difference described declared)))))))
(deftest-async every-operation-hits-the-exact-method-and-url
(loop [rows operations]
(when-let [[description f method url & args] (first rows)]
(let [captured (atom [])
client (ird/make-client {:api-key "ird_k" :http-fn (tu/capturing captured)})
result (<! (apply f client args))]
(is (not (tu/error? result)) (str description " delivered an error: " (some-> result ex-message)))
(is (= 1 (count @captured)) (str description " made exactly one request"))
(is (= method (:method (first @captured))) (str description " method"))
(is (= url (:url (first @captured))) (str description " url"))
(is (= "Bearer ird_k" (get-in (first @captured) [:headers "Authorization"])) (str description " auth")))
(recur (rest rows)))))
|