(ns ird.spec-check-test
"Offline tests for the conformance validator itself. ird.conformance-test
only proves the live API agrees with the spec if this detector actually
detects — a checker that always returns [] would pass that sweep silently
forever, so each class of drift it is meant to catch is pinned here."
(:require
[clojure.test :refer [deftest is testing]]
[ird.spec-check :as spec-check]))
(defn- check [schema value] (spec-check/check schema value ""))
(def ^:private pin-row
{"type" "object"
"additionalProperties" false
"required" ["id" "cid" "state"]
"properties" {"id" {"type" "string"}
"cid" {"oneOf" [{"type" "string"} {"type" "null"}]}
"state" {"type" "string" "enum" ["queued" "pinned" "failed"]}
"size_bytes" {"type" "integer"}}})
(deftest conforming-responses-produce-no-problems
(is (= [] (check pin-row {:id "p1" :cid "bafy1" :state "pinned" :size_bytes 56})))
(testing "an optional field may be absent, and a oneOf null branch is fine"
(is (= [] (check pin-row {:id "p1" :cid nil :state "queued"}))))
(testing "an unknown schema passes rather than guessing"
(is (= [] (check nil {:anything true})))
(is (= [] (check {} {:anything true})))))
(deftest detects-every-class-of-drift
(testing "a required key the API did not return"
(is (re-find #"\.state is required by the spec but absent"
(first (check pin-row {:id "p1" :cid "bafy1"})))))
(testing "a field the API returns that the spec does not declare"
;; the class that found registrar and target_url
(is (re-find #"\.cid_v1 is returned by the API but absent from the spec"
(first (check pin-row {:id "p1" :cid "b" :state "pinned"
:cid_v1 "bafy2"})))))
(testing "a value outside a declared enum"
;; the class that found change-rdns
(is (re-find #"not in the declared enum"
(first (check pin-row {:id "p1" :cid "b" :state "deleting"})))))
(testing "a declared type the API contradicts"
;; the class that would catch a price sent as a number instead of a string
(is (re-find #"declared integer but the API returned string"
(first (check pin-row {:id "p1" :cid "b" :state "pinned"
:size_bytes "56"})))))
(testing "a oneOf none of whose branches match"
(is (re-find #"matches none of oneOf"
(first (check pin-row {:id "p1" :cid 42 :state "pinned"}))))))
(deftest walks-nested-bodies-and-every-array-row
(let [page {"type" "object"
"properties" {"pins" {"type" "array" "items" pin-row}}}]
(testing "problems are reported with the path that locates them"
(is (= [".pins[1].state is required by the spec but absent from the response"]
(check page {:pins [{:id "a" :cid "x" :state "pinned"}
{:id "b" :cid "y"}]}))))
(testing "a late row is checked, not just the first few"
;; change-rdns first appeared at row 7 of the activity feed; sampling
;; the head of the list is exactly how that stayed hidden
(let [rows (concat (repeat 20 {:id "a" :cid "x" :state "pinned"})
[{:id "z" :cid "x" :state "not-a-state"}])]
(is (= 1 (count (check page {:pins (vec rows)}))))
(is (re-find #"\.pins\[20\]\.state" (first (check page {:pins (vec rows)}))))))))