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 | (ns ird.p2p.app-test
"The facade: option validation, descriptor caching, resolved endpoints,
ICE configuration from TURN credentials, mailbox construction rules,
join refusals."
(:require [cljs.test :refer [deftest is testing]]
[cljs.core.async :refer [<!]]
[ird.p2p :as p2p]
[ird.p2p.descriptor :as desc]
[ird.p2p.errors :as err]
[ird.p2p.fake-relay :as fake]
[ird.p2p.identity :as ident]
[ird.p2p.mailbox :as mbx]
[ird.p2p.test-util :as tu :refer [deftest-async]]
[ird.p2p.transport.ws :as ws]))
(deftest make-app-validates
(is (thrown? :default (p2p/make-app {})))
(is (thrown? :default (p2p/make-app {:host "h"})))
(is (thrown? :default (p2p/make-app {:host "h" :app-salt "s" :origin 5})))
(let [e (try (p2p/make-app {:app-salt "s"}) (catch :default e e))]
(is (err/config-error? e)))
(is (map? (p2p/make-app {:host "h" :app-salt "s"}))))
(deftest-async descriptor-is-cached-and-fallback-retried
(let [captured (atom [])
now (atom 0)
responses (atom [[500 {:error "x"}] [200 (tu/fixture "descriptor-full.json")]])
http (fn [req] (swap! captured conj req)
(let [[s b] (first @responses)] (swap! responses #(if (> (count %) 1) (subvec % 1) %))
(cljs.core.async/go {:status s :headers {} :body (tu/->json b)})))
app (p2p/make-app {:host "app.example.test" :app-salt "s" :origin "https://o.example" :http-fn http :now-fn (fn [] @now)})
d1 (<! (p2p/fetch-descriptor app))
d2 (<! (p2p/fetch-descriptor app))]
(is (:is-fallback d1))
(is (:is-fallback d2) "cached within 30 s")
(is (= 1 (count @captured)))
(swap! now + 31000)
(let [d3 (<! (p2p/fetch-descriptor app))]
(is (false? (:is-fallback d3)) "a fallback is retried after 30 s")
(is (= 2 (count @captured))))
(swap! now + 3599000)
(<! (p2p/fetch-descriptor app))
(is (= 2 (count @captured)) "a real descriptor is kept for an hour")
(swap! now + 2000)
(<! (p2p/fetch-descriptor app))
(is (= 3 (count @captured)))
(<! (p2p/fetch-descriptor app {:refresh? true}))
(is (= 4 (count @captured)))
(is (= {:ws "wss://app.example.test/ws" :turn-credentials "https://app.example.test/turn-credentials"
:mailbox "https://app.example.test/mailbox/v1" :mailbox-mint "https://app.example.test/mailbox-credentials"}
(<! (p2p/resolved-endpoints app))))))
(deftest-async endpoint-overrides-win
(let [app (p2p/make-app {:host "h" :app-salt "s" :descriptor (desc/fallback "h")
:endpoints {:ws "ws://127.0.0.1:8080/ws" :mailbox "http://127.0.0.1:8081/mailbox/v1"}})]
(is (= {:ws "ws://127.0.0.1:8080/ws" :turn-credentials "https://h/turn-credentials"
:mailbox "http://127.0.0.1:8081/mailbox/v1" :mailbox-mint nil}
(<! (p2p/resolved-endpoints app))))))
(deftest-async ice-configuration-from-turn-credentials
(let [now (atom 0)
captured (atom [])
http (tu/routing captured [["/turn-credentials" (fn [_] [200 {:username "u" :credential "c" :ttl 600 :urls ["turn:t.example:3478" "stun:t.example:3478"]}])]])
app (p2p/make-app {:host "app.example.test" :app-salt "s" :origin "https://o.example" :http-fn http :now-fn (fn [] @now)
:descriptor (desc/parse (tu/fixture "descriptor-full.json"))})
ice (<! (p2p/ice-configuration app))]
(is (= {:servers [{:urls ["turn:t.example:3478" "stun:t.example:3478"] :username "u" :credential "c"}] :relay-only? false} ice))
(is (= "https://o.example" (get-in (first @captured) [:headers "Origin"])))
(<! (p2p/ice-configuration app))
(is (= 1 (count @captured)) "cached")
(swap! now + 301000)
(<! (p2p/ice-configuration app))
(is (= 2 (count @captured)) "renewed five minutes before expiry"))
(let [app (p2p/make-app {:host "h" :app-salt "s" :http-fn (tu/responding 403 {:error "origin_not_allowed"})
:descriptor (desc/fallback "h") :force-relay? true})]
(is (= {:servers [] :relay-only? true} (<! (p2p/ice-configuration app))) "degrades to no servers")))
(deftest-async mailbox-construction-rules
(let [http (tu/responding 404 {:error "not_found"})]
(let [app (p2p/make-app {:host "h" :app-salt "s" :http-fn http :descriptor (desc/parse (tu/fixture "descriptor-minimal.json"))})
e (<! (p2p/open-mailbox app (p2p/random-secret)))]
(is (err/config-error? e))
(is (re-find #"no mailbox" (ex-message e))))
(let [app (p2p/make-app {:host "h" :app-salt "s" :http-fn http :descriptor (desc/parse (tu/fixture "descriptor-closed-mint.json"))})
e (<! (p2p/open-mailbox app (p2p/random-secret)))]
(is (err/config-error? e))
(is (re-find #"open mint is off" (ex-message e))))
(let [app (p2p/make-app {:host "h" :app-salt "s" :http-fn http :descriptor (desc/parse (tu/fixture "descriptor-closed-mint.json"))
:token-fn (mbx/static-token-fn {:token "t" :expires_at 1})})
m (<! (p2p/open-mailbox app (p2p/random-secret)))]
(is (map? m))
(is (= "https://closed.example.test/mailbox/v1" (get-in m [:client :endpoint]))))
(let [app (p2p/make-app {:host "h" :app-salt "s" :http-fn http :descriptor (desc/parse (tu/fixture "descriptor-full.json"))})
m (<! (p2p/open-mailbox app (p2p/random-secret)))]
(is (map? m) "the open mint is the default token provider"))
(is (err/config-error? (<! (p2p/open-mailbox (p2p/make-app {:host "h" :app-salt "s"}) "short"))))))
(deftest-async join-refusals
(let [relay (fake/make-relay {:reject-with 403})
app (p2p/make-app {:host "h" :app-salt "s" :descriptor (desc/fallback "h")
:transport (ws/transport {:socket-fn (fake/socket-fn relay) :sleep-fn tu/no-sleep})})
e (<! (p2p/join app (p2p/random-secret)))]
(is (err/upgrade-rejected? e))
(is (= 403 (:status (ex-data e)))))
(is (err/config-error? (<! (p2p/join (p2p/make-app {:host "h" :app-salt "s"}) "not-a-secret!")))))
(deftest invite-links
(let [s (p2p/random-secret)]
(is (= 43 (count s)))
(is (= (str "myapp://join#" s) (p2p/invite-url "myapp://join" s)))
(is (= (str "https://x.example/r?a=1#" s) (p2p/invite-url "https://x.example/r?a=1#old" s)))
(is (= s (p2p/invite-secret (p2p/invite-url "myapp://join" s))))
(is (nil? (p2p/invite-secret "myapp://join")))
(is (nil? (p2p/invite-secret "myapp://join#")))
(is (nil? (p2p/invite-secret "myapp://join#tooshort")))
(is (nil? (p2p/invite-secret "myapp://join#***********************")))
(is (= "EBESExQVFhcYGRobHB0eHw" (p2p/invite-secret "x://y#EBESExQVFhcYGRobHB0eHw")) "16 bytes is the minimum")))
|