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 | (ns ird.gate-test
(:require
[cheshire.core :as json]
[clojure.test :refer [deftest is testing]]
[ird.client :as ird]
[ird.dns :as dns]
[ird.errors :as err]
[ird.gate :as gate]
[ird.ipfs :as ipfs]
[ird.transport :as transport]))
(def registrations-page
{:registrations [{:id "reg-1" :domain "safe.ro"}
{:id "reg-2" :domain "prod.ro"}
{:id "reg-3" :domain "Sub.Prod.ro"}]
:next_cursor nil})
(defn- with-registrations-http
"An *http-fn* answering the registrations listing and recording any other
request into CAPTURED."
[captured]
(fn [req]
(if (re-find #"/domains/registrations\?" (:uri req))
{:status 200 :body (json/generate-string registrations-page)}
(do (swap! captured conj req)
{:status 200 :body "{}"}))))
(defn- gated-client [opts]
(let [captured (atom [])]
(binding [transport/*http-fn* (with-registrations-http captured)]
[(ird/make-client (merge {:api-key "ird_k"} opts)) captured])))
(defn- registration-404
"An *http-fn* answering 404 to the single-registration lookup done on a
cache miss."
[]
(fn [_]
{:status 404
:body (json/generate-string {:error {:code "not-found"
:message "no such registration"}})}))
(deftest matching-test
(testing "exact match, case-insensitive, trailing dot ignored"
(let [[client _] (gated-client {:denied-domains ["Prod.RO"]})]
(is (thrown? clojure.lang.ExceptionInfo
(gate/check! client "POST /x" {:domain "prod.ro."})))))
(testing "wildcard matches subdomains and the bare suffix"
(let [[client _] (gated-client {:denied-domains ["*.prod.ro"]})]
(is (thrown? clojure.lang.ExceptionInfo
(gate/check! client "POST /x" {:domain "api.prod.ro"})))
(is (thrown? clojure.lang.ExceptionInfo
(gate/check! client "POST /x" {:domain "prod.ro"})))
(is (nil? (gate/check! client "POST /x" {:domain "notprod.ro"})))))
(testing "allow-list denies everything not listed"
(let [[client _] (gated-client {:allowed-domains ["safe.ro"]})]
(is (nil? (gate/check! client "POST /x" {:domain "safe.ro"})))
(let [e (try (gate/check! client "POST /x" {:domain "other.ro"})
(catch clojure.lang.ExceptionInfo ex ex))]
(is (err/domain-denied? e))
(is (= "other.ro" (:domain (ex-data e)))))))
(testing "deny wins over allow"
(let [[client _] (gated-client {:allowed-domains ["*.ro"]
:denied-domains ["prod.ro"]})]
(is (nil? (gate/check! client "POST /x" {:domain "safe.ro"})))
(is (thrown? clojure.lang.ExceptionInfo
(gate/check! client "POST /x" {:domain "prod.ro"})))))
(testing "an empty allow-list denies every domain mutation"
(let [[client _] (gated-client {:allowed-domains []})]
(is (err/domain-denied?
(try (gate/check! client "POST /x" {:domain "anything.ro"})
(catch clojure.lang.ExceptionInfo ex ex)))))))
(deftest registration-id-resolution-test
(testing "ids resolve to domains through the cache built at make-client"
(let [[client _] (gated-client {:denied-domains ["prod.ro"]})]
(is (thrown? clojure.lang.ExceptionInfo
(gate/check! client "POST /y" {:registration-id "reg-2"})))
(is (nil? (gate/check! client "POST /y" {:registration-id "reg-1"})))))
(testing "cached domains are normalized"
(let [[client _] (gated-client {:denied-domains ["sub.prod.ro"]})]
(is (thrown? clojure.lang.ExceptionInfo
(gate/check! client "POST /y" {:registration-id "reg-3"})))))
(testing "cache miss fetches the single registration and applies the gate"
(let [[client _] (gated-client {:denied-domains ["prod.ro"]})]
(binding [transport/*http-fn*
(fn [req]
(is (re-find #"/domains/registrations/reg-late$" (:uri req)))
{:status 200
:body (json/generate-string
{:registration {:id "reg-late" :domain "prod.ro"}})})]
(is (thrown? clojure.lang.ExceptionInfo
(gate/check! client "POST /y" {:registration-id "reg-late"}))))
;; and the fetched id is now cached (denied again without HTTP)
(binding [transport/*http-fn* (registration-404)]
(is (thrown? clojure.lang.ExceptionInfo
(gate/check! client "POST /y" {:registration-id "reg-late"}))))))
(testing "id unknown to cache and API passes under a pure deny-list"
(let [[client _] (gated-client {:denied-domains ["prod.ro"]})]
(binding [transport/*http-fn* (registration-404)]
(is (nil? (gate/check! client "POST /y" {:registration-id "reg-unknown"}))))))
(testing "id unknown to cache and API fails closed under an allow-list"
(let [[client _] (gated-client {:allowed-domains ["safe.ro"]})]
(binding [transport/*http-fn* (registration-404)]
(let [e (try (gate/check! client "POST /y" {:registration-id "reg-unknown"})
(catch clojure.lang.ExceptionInfo ex ex))]
(is (err/domain-denied? e))
(is (nil? (:domain (ex-data e)))))))))
(deftest gateway-domain-pins-test
(testing "attaching pins to a denied gateway domain throws before the POST"
(let [[client _] (gated-client {:denied-domains ["*.prod.ro"]})
captured (atom [])]
(binding [transport/*http-fn*
(fn [req]
(swap! captured conj req)
{:status 200
:body (json/generate-string
{:domain {:id "gw-1" :domain "gw.prod.ro"}})})]
(let [e (try (ipfs/update-domain-pins client "gw-1" {:add ["pin-1"]})
(catch clojure.lang.ExceptionInfo ex ex))]
(is (err/domain-denied? e))
(is (= "gw.prod.ro" (:domain (ex-data e))))))
;; only the get-domain lookup went out; no POST reached the API
(is (= [:get] (mapv :method @captured)))
(is (= 1 (count @captured)))
(is (re-find #"/ipfs/domains/gw-1$" (:uri (first @captured))))))
(testing "an allowed gateway domain sends the delta"
(let [[client _] (gated-client {:denied-domains ["*.prod.ro"]})
captured (atom [])]
(binding [transport/*http-fn*
(fn [req]
(swap! captured conj req)
{:status 200
:body (json/generate-string
{:domain {:id "gw-2" :domain "gw.safe.ro"
:pin_count 1}})})]
(is (= 1 (get-in (ipfs/update-domain-pins client "gw-2" {:add ["pin-1"]})
[:domain :pin_count]))))
(is (= [:get :post] (mapv :method @captured)))
(is (re-find #"/ipfs/domains/gw-2/pins$" (:uri (second @captured))))
(is (= {:add ["pin-1"]}
(json/parse-string (:body (second @captured)) true)))))
(testing "an ungated client skips the lookup entirely"
(let [client (ird/make-client {:api-key "ird_k"})
captured (atom [])]
(binding [transport/*http-fn*
(fn [req] (swap! captured conj req) {:status 200 :body "{}"})]
(ipfs/update-domain-pins client "gw-3" {:remove ["pin-1"]}))
(is (= [:post] (mapv :method @captured))))))
(deftest claim-mutations-test
(testing "releasing a claim on a denied domain resolves the claim first and throws before the DELETE"
(let [[client captured] (gated-client {:denied-domains ["prod.ro"]})]
(binding [transport/*http-fn*
(fn [req]
(swap! captured conj req)
{:status 200
:body (json/generate-string {:claim {:id "c1" :domain "prod.ro"}})})]
(is (err/domain-denied?
(try (dns/release-claim client "c1")
(catch clojure.lang.ExceptionInfo ex ex)))))
(is (= [:get] (mapv :method @captured)))
(is (re-find #"/dns/claims/c1$" (:uri (first @captured))))))
(testing "seeding a claim on an allowed domain sends the body"
(let [[client captured] (gated-client {:allowed-domains ["safe.ro"]})]
(binding [transport/*http-fn*
(fn [req]
(swap! captured conj req)
{:status 200
:body (json/generate-string {:claim {:id "c2" :domain "safe.ro"}})})]
(dns/seed-claim client "c2" {:snapshot_id "s1"}))
(is (= [:get :put] (mapv :method @captured)))
(is (re-find #"/dns/claims/c2/seed$" (:uri (second @captured))))
(is (= {:snapshot_id "s1"}
(json/parse-string (:body (second @captured)) true)))))
(testing "an ungated client skips the lookup entirely"
(let [client (ird/make-client {:api-key "ird_k"})
captured (atom [])]
(binding [transport/*http-fn*
(fn [req] (swap! captured conj req) {:status 200 :body "{}"})]
(dns/unseed-claim client "c3"))
(is (= [:delete] (mapv :method @captured))))))
(deftest gate-lifecycle-test
(testing "client without gate options has no gate and check! is a no-op"
(let [client (ird/make-client {:api-key "ird_k"})]
(is (nil? (:gate client)))
(is (nil? (gate/check! client "POST /x" {:domain "anything.ro"})))))
(testing "gate options on an anonymous client throw a config error"
(is (thrown-with-msg? clojure.lang.ExceptionInfo
#"require an authenticated client"
(ird/make-client {:denied-domains ["x.ro"]}))))
(testing "gate throws before any request reaches *http-fn*"
(let [[client captured] (gated-client {:denied-domains ["prod.ro"]})]
(binding [transport/*http-fn* (with-registrations-http captured)]
(is (thrown? clojure.lang.ExceptionInfo
(gate/check! client "POST /x" {:domain "prod.ro"})))
(is (empty? @captured)))))
(testing "refresh! picks up newly registered domains"
(let [[client _] (gated-client {:allowed-domains ["safe.ro" "new.ro"]})]
(binding [transport/*http-fn* (registration-404)]
(is (err/domain-denied?
(try (gate/check! client "POST /y" {:registration-id "reg-9"})
(catch clojure.lang.ExceptionInfo ex ex)))))
(binding [transport/*http-fn*
(fn [_] {:status 200
:body (json/generate-string
{:registrations [{:id "reg-9" :domain "new.ro"}]
:next_cursor nil})})]
(gate/refresh! client))
(is (nil? (gate/check! client "POST /y" {:registration-id "reg-9"}))))))
|