cljs / test / ird / mailbox_test.cljs
805 words
  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
(ns ird.mailbox-test
  "The p2p offline mailbox: a node-hosted data plane, so these assert the
  URL shape, room encoding, payload handling and cursor semantics rather
  than appearing in the API coverage table."
  (:require [cljs.test :refer [deftest is testing]]
            [cljs.core.async :refer [<!]]
            [ird.client :as ird]
            [ird.errors :as err]
            [ird.mailbox :as mailbox]
            [ird.test-util :as tu :refer [deftest-async]]))

(def creds
  {:token "1790000000:app-1:abc:.deadbeef"
   :ttl 3600
   :expires_at 1790000000
   :endpoint "https://sueta.ap2p.ro/mailbox/v1"})

(defn- body-text [req] (.toString (js/Buffer.from (:body req))))

(deftest-async credentials-test
  ;; mints through the ird API and unwraps :credentials
  (let [captured (atom [])
        client (ird/make-client {:api-key "ird_k" :http-fn (tu/scripted captured [[200 {:credentials creds}]])})]
    (is (= creds (<! (mailbox/credentials client "app-1" {:room "r" :ttl_sec 60}))))
    (is (= "https://api.ird.ro/p2p/apps/app-1/mailbox-credentials" (:url (first @captured))))
    (is (= {:room "r" :ttl_sec 60} (tu/json-body (first @captured))))))

(deftest-async deposit-test
  ;; posts raw bytes to the node with the mailbox token
  (let [captured (atom [])
        http (tu/scripted captured [[201 {:seq "1844674407370955161" :id "ab12" :expires_at 1790086400 :deduped false}]])
        resp (<! (mailbox/deposit creds "room-1" "hello" {:http-fn http}))
        req (first @captured)]
    (is (= "1844674407370955161" (:seq resp)))
    (is (false? (:deduped resp)))
    (is (= :post (:method req)))
    (is (= "https://sueta.ap2p.ro/mailbox/v1/rooms/room-1/messages" (:url req)))
    (is (= (str "Bearer " (:token creds)) (get-in req [:headers "Authorization"])))
    (is (= "application/octet-stream" (get-in req [:headers "Content-Type"])))
    (is (= "hello" (body-text req))))
  ;; a string payload is data, not a file path
  (let [captured (atom [])]
    (<! (mailbox/deposit creds "r" "/etc/hosts" {:http-fn (tu/scripted captured [[201 {:seq "1"}]])}))
    (is (= "/etc/hosts" (body-text (first @captured)))))
  ;; a room id with slashes stays one path segment
  (let [captured (atom [])]
    (<! (mailbox/deposit creds "/orbitdb/zdpuAn Address" (js/Buffer.from "x")
                         {:http-fn (tu/scripted captured [[201 {:seq "1"}]])}))
    (is (= "https://sueta.ap2p.ro/mailbox/v1/rooms/%2Forbitdb%2FzdpuAn%20Address/messages"
           (:url (first @captured)))))
  ;; a trailing slash on the endpoint does not double up
  (let [captured (atom [])]
    (<! (mailbox/deposit (assoc creds :endpoint "https://sueta.ap2p.ro/mailbox/v1/") "r" "x"
                         {:http-fn (tu/scripted captured [[201 {:seq "1"}]])}))
    (is (= "https://sueta.ap2p.ro/mailbox/v1/rooms/r/messages" (:url (first @captured)))))
  ;; a blank room, a missing endpoint and a bad payload are refused before any request
  (let [captured (atom [])
        http (tu/capturing captured)
        e1 (<! (mailbox/deposit creds "" "x" {:http-fn http}))
        e2 (<! (mailbox/deposit (dissoc creds :endpoint) "r" "x" {:http-fn http}))
        e3 (<! (mailbox/deposit creds "r" 42 {:http-fn http}))]
    (is (err/config-error? e1))
    (is (re-find #"room must be" (ex-message e1)))
    (is (err/config-error? e2))
    (is (re-find #"must carry an :endpoint" (ex-message e2)))
    (is (err/transport-error? e3))
    (is (empty? @captured)))
  ;; the node's bare {"error": code} envelope maps to :ird/api-error
  (let [e (<! (mailbox/deposit creds "r" "x" {:http-fn (tu/responding 413 {:error "message_too_large"})}))]
    (is (err/api-error? e))
    (is (= "message_too_large" (:code (ex-data e))))
    (is (= 413 (:status (ex-data e))))
    (is (= "message_too_large" (ex-message e)))))

(deftest-async replay-test
  ;; cursor and limit ride the query string
  (let [captured (atom [])]
    (<! (mailbox/replay creds "r" {:after "1844674407370955161" :limit 500
                                   :http-fn (tu/responding captured 200 {:messages [] :has_more false
                                                                         :oldest_seq "0" :latest_seq "0"})}))
    (is (= "https://sueta.ap2p.ro/mailbox/v1/rooms/r/messages?after=1844674407370955161&limit=500"
           (:url (first @captured))))
    (is (= :get (:method (first @captured)))))
  ;; payload decoding
  (let [page (<! (mailbox/replay creds "r"
                                 {:http-fn (tu/responding 200 {:messages [{:seq "5" :id "ab" :ts 1 :payload "aGVsbG8="}]
                                                               :has_more false :oldest_seq "5" :latest_seq "5"})}))
        msg (first (:messages page))]
    (is (= "hello" (mailbox/payload-string msg)))
    (is (= [104 101 108 108 111] (vec (array-seq (mailbox/payload-bytes msg)))))))

(deftest history-gap-test
  (testing "a cursor older than :oldest_seq is a truncated history"
    (is (true? (mailbox/history-gap? "100" {:oldest_seq "200"}))))
  (testing "seqs compare as integers, not strings or doubles"
    (is (false? (mailbox/history-gap? "1099511627776" {:oldest_seq "99"})))
    (is (true? (mailbox/history-gap? "99" {:oldest_seq "1099511627776"})))
    (is (true? (mailbox/history-gap? "18446744073709551615" {:oldest_seq "18446744073709551616"})))
    (is (false? (mailbox/history-gap? "18446744073709551616" {:oldest_seq "18446744073709551615"}))))
  (testing "a cursor at or after :oldest_seq is no gap"
    (is (false? (mailbox/history-gap? "200" {:oldest_seq "200"})))
    (is (false? (mailbox/history-gap? "300" {:oldest_seq "200"}))))
  (testing "a first visit and an empty room are never gaps"
    (is (false? (mailbox/history-gap? nil {:oldest_seq "200"})))
    (is (false? (mailbox/history-gap? "" {:oldest_seq "200"})))
    (is (false? (mailbox/history-gap? "5" {:oldest_seq "0"})))
    (is (false? (mailbox/history-gap? "5" {})))))

(deftest-async replay-all-test
  ;; pages on :has_more, advancing :after by the last :seq
  (let [captured (atom [])
        http (tu/scripted captured
                          [[200 {:messages [{:seq "10" :payload "YQ=="} {:seq "20" :payload "Yg=="}]
                                 :has_more true :oldest_seq "10" :latest_seq "30"}]
                           [200 {:messages [{:seq "30" :payload "Yw=="}]
                                 :has_more false :oldest_seq "10" :latest_seq "30"}]])
        {:keys [messages cursor gap?]} (<! (mailbox/replay-all creds "r" {:limit 2 :http-fn http}))]
    (is (= ["10" "20" "30"] (mapv :seq messages)))
    (is (= "30" cursor))
    (is (false? gap?))
    (is (= ["https://sueta.ap2p.ro/mailbox/v1/rooms/r/messages?limit=2"
            "https://sueta.ap2p.ro/mailbox/v1/rooms/r/messages?after=20&limit=2"]
           (mapv :url @captured))))
  ;; a quiet room keeps the cursor it was given
  (let [result (<! (mailbox/replay-all creds "r" {:after "9"
                                                  :http-fn (tu/responding 200 {:messages [] :has_more false
                                                                               :oldest_seq "5" :latest_seq "9"})}))]
    (is (empty? (:messages result)))
    (is (= "9" (:cursor result)))
    (is (false? (:gap? result))))
  ;; an evicted cursor is reported as a gap, from the first page
  (let [result (<! (mailbox/replay-all creds "r" {:after "10"
                                                  :http-fn (tu/responding 200 {:messages [{:seq "50" :payload "YQ=="}]
                                                                               :has_more false :oldest_seq "40" :latest_seq "50"})}))]
    (is (true? (:gap? result))))
  ;; an error on a page is delivered
  (let [e (<! (mailbox/replay-all creds "r" {:http-fn (tu/responding 401 {:error "unauthorized"})}))]
    (is (err/api-error? e))))

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/cljs.git