(ns ird.spec-check-test
"Offline tests for the conformance validator itself. The live conformance
sweep only proves the 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 [cljs.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"
(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"
(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"
(is (re-find #"declared integer but the API returned string"
(first (check pin-row {:id "p1" :cid "b" :state "pinned" :size_bytes "56"}))))
(is (re-find #"declared integer but the API returned number"
(first (check pin-row {:id "p1" :cid "b" :state "pinned" :size_bytes 56.5})))))
(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"
(let [rows (vec (concat (repeat 20 {:id "a" :cid "x" :state "pinned"})
[{:id "z" :cid "x" :state "not-a-state"}]))]
(is (= 1 (count (check page {:pins rows}))))
(is (re-find #"\.pins\[20\]\.state" (first (check page {:pins rows}))))))))
(deftest response-schema-lookup
(let [spec {"paths" {"/x" {"get" {"responses" {"200" {"content" {"application/json" {"schema" {"type" "object"}}}}}}}}}]
(is (= {"type" "object"} (spec-check/response-schema spec :get "/x" 200)))
(is (nil? (spec-check/response-schema spec :get "/x" 404)))
(is (nil? (spec-check/response-schema spec :post "/x" 200)))))