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 | (ns ird.live.conformance-test
"Live spec-conformance sweep: call every read endpoint against
https://api.ird.ro and validate each response body against the schema
`spec/openapi.json` declares for it.
This is the complement to ird.coverage-test. That one asserts the method
and URL of all 161 operations against a capturing transport, which pins
the client to the spec but says nothing about whether the spec still
matches the API. This one closes that gap.
Compiled only by the :live build (`npm run test:live`); skips itself
without IRD_API_KEY. Read-only by design: it creates nothing and charges
nothing beyond metered reads. Endpoints that need a resource to exist are
exercised against whatever the account already has and skipped when it
has none — the note at the bottom lists what that leaves out."
(:require [clojure.string :as str]
[cljs.test :refer [is]]
[cljs.core.async :refer [<!]]
[ird.async :as a]
[ird.client :as ird]
[ird.credits :as credits]
[ird.dns :as dns]
[ird.domains :as domains]
[ird.ipfs :as ipfs]
[ird.p2p :as p2p]
[ird.pins :as pins]
[ird.registrations :as reg]
[ird.sharing :as sharing]
[ird.spec-check :as spec-check]
[ird.support :as support]
[ird.test-util :as tu :refer [deftest-async]]
[ird.transfers :as transfers]
[ird.tunnels :as tunnels]
[ird.vps :as vps]))
(defn- first-of
"Channel of the first item under COLLECTION-KEY of the listing CH, or nil
when the account has none (or the listing failed)."
[ch collection-key]
(a/go-try
(let [r (<! ch)]
(when-not (a/error? r) (first (get r collection-key))))))
(defn- endpoints
"Channel of [spec-path expected-status thunk] rows for every read endpoint
reachable with this account's data; thunks return channels. A few listings
are account-JWT-only and answer 401 to an API key; those carry 401 as their
expected status, so their error envelope is validated instead."
[client anon]
(a/go-try
(let [registrations (or (:registrations (<! (reg/list-registrations client {:limit 100}))) [])
reg-id (:id (first registrations))
zones (let [r (<! (dns/list-zones client))] (when-not (a/error? r) (:domains r)))
zone (some-> (first zones) :origin (str/replace #"\.$" ""))
hosted-reg-id (let [origins (set (map #(str/replace (:origin %) #"\.$" "") zones))]
(:id (first (filter #(contains? origins (:domain %)) registrations))))
claim-id (:id (<! (first-of (dns/list-claims client) :claims)))
gw-id (:id (<! (first-of (ipfs/list-domains client) :domains)))
name-id (:id (<! (first-of (ipfs/list-names client) :names)))
pin-id (:id (<! (first-of (ipfs/list-pins client) :pins)))
app-id (:id (<! (first-of (p2p/list-apps client) :apps)))
tunnel-id (:id (<! (first-of (tunnels/list-tunnels client) :tunnels)))
instance-id (:id (<! (first-of (vps/list-instances client) :instances)))
deployment-id (:id (<! (first-of (vps/list-app-deployments client) :deployments)))
action-id (:id (<! (first-of (reg/list-actions client) :actions)))
thread-id (:id (<! (first-of (support/list-threads client) :threads)))]
(->> [["/credits/balance" 200 #(credits/balance client)]
["/credits/ledger" 200 #(credits/ledger client)]
["/credits/prices" 200 #(credits/prices client)]
["/contact/threads" 200 #(support/list-threads client)]
["/contact/threads/{id}" 200 (when thread-id #(support/get-thread client thread-id))]
["/domains/check" 200 #(domains/check anon "ird.ro")]
["/domains/info" 200 #(domains/info client "ird.ro")]
["/domains/preflight" 200 #(domains/preflight client "ird.ro")]
["/domains/dns-snapshot" 200 #(domains/dns-snapshot client "ird.ro")]
["/domains/search" 200 #(domains/search client "shop")]
["/domains/example" 200 #(domains/example client)]
["/domains/directory" 200 #(domains/directory client {:limit 20})]
["/domains/directory/{domain}" 200 #(domains/directory-entry client "ird.ro")]
["/domains/registrations" 200 #(reg/list-registrations client {:limit 100})]
["/domains/registrations/{id}" 200 (when reg-id #(reg/get-registration client reg-id))]
["/domains/registrations/{id}/hosts" 200 (when reg-id #(reg/list-hosts client reg-id))]
["/domains/registrations/{id}/dsdata" 200 (when reg-id #(reg/list-dsdata client reg-id))]
["/domains/registrations/{id}/trade" 200 (when reg-id #(reg/quote-trade client reg-id))]
["/domains/registrations/{id}/dns/dnssec" 200 (when reg-id #(dns/get-dnssec client reg-id))]
["/domains/registrations/{id}/dns/nameservers" 200 (when hosted-reg-id #(dns/hosted-nameservers client hosted-reg-id))]
["/domains/actions" 200 #(reg/list-actions client {:limit 100})]
["/domains/actions/{id}" 200 (when action-id #(reg/get-action client action-id))]
["/dns/domains" 200 #(dns/list-zones client)]
["/dns/domains/{domain}" 200 (when zone #(dns/get-zone client zone))]
["/dns/claims" 200 #(dns/list-claims client)]
["/dns/claims/{id}" 200 (when claim-id #(dns/get-claim client claim-id))]
["/ipfs/pins" 200 #(ipfs/list-pins client {:limit 100})]
["/ipfs/pins/{id}" 200 (when pin-id #(ipfs/get-pin client pin-id))]
["/ipfs/names" 200 #(ipfs/list-names client)]
["/ipfs/names/{id}" 200 (when name-id #(ipfs/get-name client name-id))]
["/ipfs/domains" 200 #(ipfs/list-domains client)]
["/ipfs/domains/{id}" 200 (when gw-id #(ipfs/get-domain client gw-id))]
["/ipfs/domains/{id}/pins" 200 (when gw-id #(ipfs/list-domain-pins client gw-id))]
["/ipfs/usage" 200 #(ipfs/usage client)]
["/pins" 200 #(pins/list-pins client {:limit 100})]
["/p2p/apps" 200 #(p2p/list-apps client)]
["/p2p/apps/{id}" 200 (when app-id #(p2p/get-app client app-id))]
["/p2p/apps/{id}/domains" 200 (when app-id #(p2p/list-app-domains client app-id))]
["/p2p/usage" 200 #(p2p/usage client)]
["/tunnels" 200 #(tunnels/list-tunnels client)]
["/tunnels/{id}" 200 (when tunnel-id #(tunnels/get-tunnel client tunnel-id))]
["/vps/tiers" 200 #(vps/list-tiers client)]
["/vps/system-images" 200 #(vps/list-system-images client)]
["/vps/ssh-keys" 200 #(vps/list-ssh-keys client)]
["/vps/availability" 200 #(vps/availability client)]
["/vps/instances" 200 #(vps/list-instances client)]
["/vps/instances/{id}" 200 (when instance-id #(vps/get-instance client instance-id))]
["/vps/instances/{id}/metrics" 200 (when instance-id #(vps/get-metrics client instance-id))]
["/vps/instances/{id}/domains" 200 (when instance-id #(vps/list-instance-domains client instance-id))]
["/vps/apps" 200 #(vps/app-catalog client)]
["/vps/apps/deployments" 200 #(vps/list-app-deployments client)]
["/vps/apps/deployments/{id}" 200 (when deployment-id #(vps/get-app-deployment client deployment-id))]
["/vps/volumes" 200 #(vps/list-volumes client)]
["/vps/images" 200 #(vps/list-images client)]
["/vps/activity" 200 #(vps/list-activity client)]
;; account-JWT-only: the 401 envelope is the documented response here
["/grants" 401 #(sharing/list-grants client)]
["/domains/registrations/{id}/grants" 401 (when reg-id #(sharing/list-domain-grants client reg-id))]
["/vps/transfers" 401 #(transfers/list-transfers client)]]
(remove (comp nil? #(nth % 2)))
vec))))
(defn- sweep
"Channel of [{:path :status :expected :problems}] — each endpoint called
and its body validated."
[spec client anon]
(a/go-try
(loop [rows (<! (endpoints client anon))
acc []]
(if-let [[path expected thunk] (first rows)]
(let [result (<! (thunk))
error? (a/error? result)
status (if error? (:status (ex-data result)) (:ird/status (meta result) 200))
body (if error? (:response (ex-data result)) result)]
(recur (rest rows)
(conj acc {:path path
:status status
:expected expected
:problems (if (= status expected)
(spec-check/check (spec-check/response-schema spec :get path status) body "")
[(str "expected HTTP " expected " but got " status
(when error? (str ": " (ex-message result))))])})))
acc))))
(deftest-async live-responses-match-the-spec
{:timeout-ms 300000}
(if-let [key (tu/api-key)]
(let [spec (tu/spec)
results (<! (sweep spec (ird/make-client {:api-key key :timeout-ms 30000}) (ird/make-client {})))
drifted (remove (comp empty? :problems) results)]
(doseq [{:keys [path problems]} drifted]
(println "\nDRIFT" path)
(doseq [p problems] (println " -" p)))
(println (str "\nconformance: " (count results) " endpoints, "
(- (count results) (count drifted)) " conform, " (count drifted) " drifted"))
(is (seq results) "no endpoints were exercised")
(is (empty? drifted)
(str "spec drift on " (pr-str (mapv :path drifted))
" — the spec and the live API disagree; see the report above")))
(println "IRD_API_KEY not set - skipping the conformance sweep")))
;; Not covered here, and why. Each needs a resource that only exists mid-flight
;; or costs money to create, so reaching it means a deliberate manual run:
;;
;; GET /jobs/{id}, /jobs/{id}/result enqueue a job (jobs/run)
;; GET /ipfs/uploads/{id} start an upload (ipfs/add-file)
;; GET /pins/{requestid} add a pin through ird.pins
;; GET /vps/actions/{id} any VPS action; create a server
;; GET /dns/claims/{id} runs whenever the account has a claim
;; GET .../dns/nameservers runs whenever a registration is on ird DNS
|