From 7fac0f1eb99348ca3c40d3b89c52a41312e97e8a Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Wed, 21 Jul 2021 12:35:38 +0200 Subject: [PATCH] [#947] Vault tests, part 1 (#949) --- deps.edn | 3 +- src/babashka/impl/classes.clj | 46 ++ src/babashka/main.clj | 43 +- .../lib_tests/babashka/run_all_libtests.clj | 11 + .../lib_tests/vault/client/http_test.clj | 198 +++++++ .../lib_tests/vault/client/mock_test.clj | 74 +++ .../vault/client/secret-fixture-logical.edn | 2 + test-resources/lib_tests/vault/env_test.clj | 33 ++ test-resources/lib_tests/vault/lease_test.clj | 98 ++++ .../lib_tests/vault/secrets/kvv1_test.clj | 183 +++++++ .../lib_tests/vault/secrets/kvv2_test.clj | 493 ++++++++++++++++++ .../vault/secrets/secret-fixture-kvv2.edn | 12 + 12 files changed, 1153 insertions(+), 43 deletions(-) create mode 100644 test-resources/lib_tests/vault/client/http_test.clj create mode 100644 test-resources/lib_tests/vault/client/mock_test.clj create mode 100644 test-resources/lib_tests/vault/client/secret-fixture-logical.edn create mode 100644 test-resources/lib_tests/vault/env_test.clj create mode 100644 test-resources/lib_tests/vault/lease_test.clj create mode 100644 test-resources/lib_tests/vault/secrets/kvv1_test.clj create mode 100644 test-resources/lib_tests/vault/secrets/kvv2_test.clj create mode 100644 test-resources/lib_tests/vault/secrets/secret-fixture-kvv2.edn diff --git a/deps.edn b/deps.edn index f2a0546d..8d8239af 100644 --- a/deps.edn +++ b/deps.edn @@ -86,7 +86,8 @@ io.replikativ/hasch {:mvn/version "0.3.7"} com.grammarly/omniconf {:mvn/version "0.4.3"} crispin/crispin {:mvn/version "0.3.8"} - org.clojure/data.json {:mvn/version "2.4.0"}} + org.clojure/data.json {:mvn/version "2.4.0"} + amperity/vault-clj {:mvn/version "1.0.4"}} :classpath-overrides {org.clojure/clojure nil org.clojure/spec.alpha nil org.clojure/core.specs.alpha nil}} diff --git a/src/babashka/impl/classes.clj b/src/babashka/impl/classes.clj index 2c19cc7d..ac1090fb 100644 --- a/src/babashka/impl/classes.clj +++ b/src/babashka/impl/classes.clj @@ -150,6 +150,7 @@ java.lang.StringBuilder java.lang.System java.lang.Throwable + ;; java.lang.UnsupportedOperationException java.math.BigDecimal java.math.BigInteger java.math.MathContext @@ -206,6 +207,8 @@ java.security.SecureRandom java.sql.Date java.text.ParseException + ;; adds about 200kb, same functionality provided by java.time: + ;; java.text.SimpleDateFormat ~@(when features/java-time? `[java.time.format.DateTimeFormatter java.time.Clock @@ -396,6 +399,49 @@ (def class-map (gen-class-map)) +(def imports + '{Appendable java.lang.Appendable + ArithmeticException java.lang.ArithmeticException + AssertionError java.lang.AssertionError + BigDecimal java.math.BigDecimal + BigInteger java.math.BigInteger + Boolean java.lang.Boolean + Byte java.lang.Byte + Character java.lang.Character + CharSequence java.lang.CharSequence + Class java.lang.Class + ClassNotFoundException java.lang.ClassNotFoundException + Comparable java.lang.Comparable + Double java.lang.Double + Exception java.lang.Exception + IndexOutOfBoundsException java.lang.IndexOutOfBoundsException + IllegalArgumentException java.lang.IllegalArgumentException + IllegalStateException java.lang.IllegalStateException + Integer java.lang.Integer + InterruptedException java.lang.InterruptedException + Iterable java.lang.Iterable + File java.io.File + Float java.lang.Float + Long java.lang.Long + Math java.lang.Math + NullPointerException java.lang.NullPointerException + Number java.lang.Number + NumberFormatException java.lang.NumberFormatException + Object java.lang.Object + Runtime java.lang.Runtime + RuntimeException java.lang.RuntimeException + Process java.lang.Process + ProcessBuilder java.lang.ProcessBuilder + Short java.lang.Short + StackTraceElement java.lang.StackTraceElement + String java.lang.String + StringBuilder java.lang.StringBuilder + System java.lang.System + Thread java.lang.Thread + Throwable java.lang.Throwable + ;; UnsupportedOperationException java.lang.UnsupportedOperationException + }) + (defn reflection-file-entries [] (let [entries (vec (for [c (sort (:all classes)) :let [class-name (str c)]] diff --git a/src/babashka/main.clj b/src/babashka/main.clj index 13d34514..a18b4071 100644 --- a/src/babashka/main.clj +++ b/src/babashka/main.clj @@ -415,47 +415,6 @@ Use bb run --help to show this help output. 'selmer.validator @(resolve 'babashka.impl.selmer/selmer-validator-namespace)))) -(def imports - '{Appendable java.lang.Appendable - ArithmeticException java.lang.ArithmeticException - AssertionError java.lang.AssertionError - BigDecimal java.math.BigDecimal - BigInteger java.math.BigInteger - Boolean java.lang.Boolean - Byte java.lang.Byte - Character java.lang.Character - CharSequence java.lang.CharSequence - Class java.lang.Class - ClassNotFoundException java.lang.ClassNotFoundException - Comparable java.lang.Comparable - Double java.lang.Double - Exception java.lang.Exception - IndexOutOfBoundsException java.lang.IndexOutOfBoundsException - IllegalArgumentException java.lang.IllegalArgumentException - IllegalStateException java.lang.IllegalStateException - Integer java.lang.Integer - InterruptedException java.lang.InterruptedException - Iterable java.lang.Iterable - File java.io.File - Float java.lang.Float - Long java.lang.Long - Math java.lang.Math - NullPointerException java.lang.NullPointerException - Number java.lang.Number - NumberFormatException java.lang.NumberFormatException - Object java.lang.Object - Runtime java.lang.Runtime - RuntimeException java.lang.RuntimeException - Process java.lang.Process - ProcessBuilder java.lang.ProcessBuilder - Short java.lang.Short - StackTraceElement java.lang.StackTraceElement - String java.lang.String - StringBuilder java.lang.StringBuilder - System java.lang.System - Thread java.lang.Thread - Throwable java.lang.Throwable}) - (def edn-readers (cond-> {} features/yaml? (assoc 'ordered/map @(resolve 'flatland.ordered.map/ordered-map)))) @@ -746,7 +705,7 @@ Use bb run --help to show this help output. :env env :features #{:bb :clj} :classes classes/class-map - :imports imports + :imports classes/imports :load-fn load-fn :uberscript uberscript :readers core/data-readers diff --git a/test-resources/lib_tests/babashka/run_all_libtests.clj b/test-resources/lib_tests/babashka/run_all_libtests.clj index 31b98f02..e08dd716 100644 --- a/test-resources/lib_tests/babashka/run_all_libtests.clj +++ b/test-resources/lib_tests/babashka/run_all_libtests.clj @@ -237,6 +237,17 @@ (test-namespaces 'clojure.data.json-test 'clojure.data.json-test-suite-test) +(test-namespaces + ;; TODO: env tests don't work because envoy lib isn't compatible with bb + #_'vault.env-test + 'vault.lease-test + 'vault.client.http-test + ;; TODO: + ;; failing tests in the following namespaces: + #_'vault.client.mock-test + #_'vault.secrets.kvv1-test + #_'vault.secrets.kvv2-test) + ;;;; final exit code (let [{:keys [:test :fail :error] :as m} @status] diff --git a/test-resources/lib_tests/vault/client/http_test.clj b/test-resources/lib_tests/vault/client/http_test.clj new file mode 100644 index 00000000..434f3039 --- /dev/null +++ b/test-resources/lib_tests/vault/client/http_test.clj @@ -0,0 +1,198 @@ +(ns vault.client.http-test + (:require + [clojure.test :refer [deftest testing is]] + [vault.authenticate :as authenticate] + [vault.client.api-util :as api-util] + [vault.client.http :refer [http-client]] + [vault.core :as vault] + [vault.secrets.kvv1 :as vault-kvv1])) + + +(def example-url "https://vault.example.com") + + +(deftest http-client-instantiation + (is (thrown? IllegalArgumentException + (http-client nil))) + (is (thrown? IllegalArgumentException + (http-client :foo))) + (is (instance? vault.client.http.HTTPClient (http-client example-url)))) + + +(deftest http-read-checks + (let [client (http-client example-url)] + (is (thrown? IllegalArgumentException + (vault-kvv1/read-secret client nil)) + "should throw an exception on non-string path") + (is (thrown? RuntimeException + (vault-kvv1/read-secret client "secret/foo/bar")) + "should throw an exception on unauthenticated client"))) + + +(deftest app-role + (let [api-endpoint (str example-url "/v1/auth/approle/login") + client (http-client example-url) + connection-attempt (atom nil)] + (with-redefs [api-util/do-api-request (fn [_method url _req] + (reset! connection-attempt url)) + authenticate/api-auth! (constantly nil)] + (vault/authenticate! client :app-role {:secret-id "secret" + :role-id "role-id"}) + (is (= @connection-attempt api-endpoint) + (str "should attempt to auth with: " api-endpoint))))) + + +(deftest authenticate-via-k8s + (testing "When a token file is available" + (let [client (http-client example-url) + api-requests (atom []) + api-auths (atom [])] + (with-redefs [api-util/do-api-request (fn [& args] + (swap! api-requests conj args) + :do-api-request-response) + authenticate/api-auth! (fn [& args] + (swap! api-auths conj args) + :api-auth!-response)] + (vault/authenticate! client :k8s {:jwt "fake-jwt-goes-here" + :role "my-role"}) + (is (= [[:post + (str example-url "/v1/auth/kubernetes/login") + {:form-params {:jwt "fake-jwt-goes-here" :role "my-role"} + :content-type :json + :accept :json}]] + @api-requests)) + (is (= [[(str "Kubernetes auth role=my-role") + (:auth client) + :do-api-request-response]] + @api-auths))))) + (testing "When no jwt is specified" + (let [client (http-client example-url) + api-requests (atom []) + api-auths (atom [])] + (with-redefs [api-util/do-api-request (fn [& args] + (swap! api-requests conj args)) + authenticate/api-auth! (fn [& args] + (swap! api-auths conj args))] + (is (thrown? IllegalArgumentException + (vault/authenticate! client :k8s {:role "my-role"}))) + (is (empty? @api-requests)) + (is (empty? @api-auths))))) + (testing "When no role is specified" + (let [client (http-client example-url) + api-requests (atom []) + api-auths (atom [])] + (with-redefs [api-util/do-api-request (fn [& args] + (swap! api-requests conj args)) + authenticate/api-auth! (fn [& args] + (swap! api-auths conj args))] + (is (thrown? IllegalArgumentException + (vault/authenticate! client :k8s {:jwt "fake-jwt-goes-here"}))) + (is (empty? @api-requests)) + (is (empty? @api-auths)))))) + + +(deftest authenticate-via-aws + (testing "When all parameters are specified" + (let [client (http-client example-url) + api-requests (atom []) + api-auths (atom [])] + (with-redefs [api-util/do-api-request (fn [& args] + (swap! api-requests conj args) + :do-api-request-response) + authenticate/api-auth! (fn [& args] + (swap! api-auths conj args) + :api-auth!-response)] + (vault/authenticate! client :aws-iam {:role "my-role" + :http-request-method "POST" + :request-url "fake.sts.com" + :request-body "FakeAction&Version=1" + :request-headers "{'foo':'bar'}"}) + (is (= [[:post + (str example-url "/v1/auth/aws/login") + {:form-params {:iam_http_request_method "POST" + :iam_request_url "fake.sts.com" + :iam_request_body "FakeAction&Version=1" + :iam_request_headers "{'foo':'bar'}" + :role "my-role"} + :content-type :json + :accept :json}]] + @api-requests)) + (is (= [["AWS auth role=my-role" + (:auth client) + :do-api-request-response]] + @api-auths))))) + (testing "When no http-request-method is specified" + (let [client (http-client example-url) + api-requests (atom []) + api-auths (atom [])] + (with-redefs [api-util/do-api-request (fn [& args] + (swap! api-requests conj args)) + authenticate/api-auth! (fn [& args] + (swap! api-auths conj args))] + (is (thrown? IllegalArgumentException + (vault/authenticate! client :aws-iam {:role "my-role" + :request-url "fake.sts.com" + :request-body "FakeAction&Version=1" + :request-headers "{'foo':'bar'}"}))) + (is (empty? @api-requests)) + (is (empty? @api-auths))))) + (testing "When no request-url is specified" + (let [client (http-client example-url) + api-requests (atom []) + api-auths (atom [])] + (with-redefs [api-util/do-api-request (fn [& args] + (swap! api-requests conj args)) + authenticate/api-auth! (fn [& args] + (swap! api-auths conj args))] + (is (thrown? IllegalArgumentException + (vault/authenticate! client :aws-iam {:role "my-role" + :http-request-method "POST" + :request-body "FakeAction&Version=1" + :request-headers "{'foo':'bar'}"}))) + (is (empty? @api-requests)) + (is (empty? @api-auths))))) + (testing "When no request-body is specified" + (let [client (http-client example-url) + api-requests (atom []) + api-auths (atom [])] + (with-redefs [api-util/do-api-request (fn [& args] + (swap! api-requests conj args)) + authenticate/api-auth! (fn [& args] + (swap! api-auths conj args))] + (is (thrown? IllegalArgumentException + (vault/authenticate! client :aws-iam {:role "my-role" + :http-request-method "POST" + :request-url "fake.sts.com" + :request-headers "{'foo':'bar'}"}))) + (is (empty? @api-requests)) + (is (empty? @api-auths))))) + (testing "When no request-headers is specified" + (let [client (http-client example-url) + api-requests (atom []) + api-auths (atom [])] + (with-redefs [api-util/do-api-request (fn [& args] + (swap! api-requests conj args)) + authenticate/api-auth! (fn [& args] + (swap! api-auths conj args))] + (is (thrown? IllegalArgumentException + (vault/authenticate! client :aws-iam {:role "my-role" + :http-request-method "POST" + :request-url "fake.sts.com" + :request-body "FakeAction&Version=1"}))) + (is (empty? @api-requests)) + (is (empty? @api-auths))))) + (testing "When no role is specified" + (let [client (http-client example-url) + api-requests (atom []) + api-auths (atom [])] + (with-redefs [api-util/do-api-request (fn [& args] + (swap! api-requests conj args)) + authenticate/api-auth! (fn [& args] + (swap! api-auths conj args))] + (is (thrown? IllegalArgumentException + (vault/authenticate! client :aws-iam {:http-request-method "POST" + :request-url "fake.sts.com" + :request-body "FakeAction&Version=1" + :request-headers "{'foo':'bar'}"}))) + (is (empty? @api-requests)) + (is (empty? @api-auths)))))) diff --git a/test-resources/lib_tests/vault/client/mock_test.clj b/test-resources/lib_tests/vault/client/mock_test.clj new file mode 100644 index 00000000..f525b0a7 --- /dev/null +++ b/test-resources/lib_tests/vault/client/mock_test.clj @@ -0,0 +1,74 @@ +(ns vault.client.mock-test + (:require + [clojure.string :as str] + [clojure.test :refer [deftest testing is]] + [vault.core :as vault]) + (:import + (clojure.lang + ExceptionInfo))) + + +(defn mock-client-authenticated + "A mock vault client using the secrets found in the given path, defaults to `vault/client/secret-fixture-logical.edn`" + ([path] + (let [client (vault/new-client (str "mock:" path))] + (vault/authenticate! client :token "fake-token") + client)) + ([] + (mock-client-authenticated "vault/client/secret-fixture-logical.edn"))) + + +(deftest create-token!-test + (testing "The return value of create-token is correct when not wrapped" + (let [result (vault/create-token! (mock-client-authenticated) {:no-default-policy true})] + (is (= ["root"] (:policies result))) + (is (= false (:renewable result))) + (is (= "" (:entity-id result))) + (is (= ["root"] (:token-policies result))) + (is (not (str/blank? (:accessor result)))) + (is (= 0 (:lease-duration result))) + (is (= "service" (:token-type result))) + (is (= false (:orphan result))) + (is (not (str/blank? (:client-token result)))) + (is (contains? result :metadata)))) + (testing "The return value of create-token is correct when not wrapped and some options are specified" + (let [result (vault/create-token! (mock-client-authenticated) {:policies ["hello" "goodbye"] + :ttl "7d"})] + (is (= ["default" "hello" "goodbye"] (:policies result))) + (is (= false (:renewable result))) + (is (= "" (:entity-id result))) + (is (= ["default" "hello" "goodbye"] (:token-policies result))) + (is (not (str/blank? (:accessor result)))) + (is (= 604800 (:lease-duration result))) + (is (= "service" (:token-type result))) + (is (= false (:orphan result))) + (is (not (str/blank? (:client-token result)))) + (is (contains? result :metadata)))) + (testing "The client throws a helpful error for debugging if ttl is incorrectly formatted" + (is (thrown-with-msg? ExceptionInfo + #"Mock Client doesn't recognize format of ttl" + (vault/create-token! (mock-client-authenticated) {:ttl "BLT"})))) + (testing "The return value of create-token is correct when not wrapped and some less common options are specified" + (let [result (vault/create-token! (mock-client-authenticated) {:policies ["hello" "goodbye"] + :ttl "10s" + :no-parent true + :no-default-policy true + :renewable true})] + (is (= ["hello" "goodbye"] (:policies result))) + (is (= true (:renewable result))) + (is (= "" (:entity-id result))) + (is (= ["hello" "goodbye"] (:token-policies result))) + (is (not (str/blank? (:accessor result)))) + (is (= 10 (:lease-duration result))) + (is (= "service" (:token-type result))) + (is (= true (:orphan result))) + (is (not (str/blank? (:client-token result)))) + (is (contains? result :metadata)))) + (testing "The return value of create-token is correct when wrapped" + (let [result (vault/create-token! (mock-client-authenticated) {:wrap-ttl "2h"})] + (is (not (str/blank? (:token result)))) + (is (not (str/blank? (:accessor result)))) + (is (= 7200 (:ttl result))) + (is (not (str/blank? (:creation-time result)))) + (is (= "auth/token/create" (:creation-path result))) + (is (not (str/blank? (:wrapped-accessor result))))))) diff --git a/test-resources/lib_tests/vault/client/secret-fixture-logical.edn b/test-resources/lib_tests/vault/client/secret-fixture-logical.edn new file mode 100644 index 00000000..6cab7c2a --- /dev/null +++ b/test-resources/lib_tests/vault/client/secret-fixture-logical.edn @@ -0,0 +1,2 @@ +{"identities" {:batman "Bruce Wayne" + :captain-marvel "Carol Danvers"}} diff --git a/test-resources/lib_tests/vault/env_test.clj b/test-resources/lib_tests/vault/env_test.clj new file mode 100644 index 00000000..91e53c25 --- /dev/null +++ b/test-resources/lib_tests/vault/env_test.clj @@ -0,0 +1,33 @@ +(ns vault.env-test + (:require + [clojure.test :refer [deftest is]] + [vault.client.mock :refer [mock-client]] + [vault.env :as venv])) + + +(deftest uri-resolution + (let [client (mock-client {"some/path" {:id "foo"}})] + (is (thrown? Exception (venv/resolve-uri nil "vault:some/path#id")) + "resolution without client should throw") + (is (thrown? Exception (venv/resolve-uri client "vault:some/path#nope")) + "resoultion of nil secret should throw") + (is (= "foo" (venv/resolve-uri client "vault:some/path#id"))))) + + +(deftest env-loading + (let [client (mock-client {"secret/foo" {:thing 123} + "secret/bar" {:id "abc"}}) + env {:a "12345" + :b "vault:secret/foo#thing" + :c "vault:secret/bar#id"}] + (is (identical? env (venv/load! client env nil)) + "resolution without whitelisted secrets returns env unchanged") + (is (= {:a "12345", :b 123, :c "abc"} + (venv/load! client env #{:a :b :c})) + "resolution allows direct passthrough") + (is (= {:a "12345", :b 123, :c "vault:secret/bar#id"} + (venv/load! client env #{:b})) + "resolution does not touch non-whitelisted vars") + (is (= {:a "12345", :b 123, :c "abc"} + (venv/load! client env #{:b :c :d})) + "resolution ignores missing whitelisted vars"))) diff --git a/test-resources/lib_tests/vault/lease_test.clj b/test-resources/lib_tests/vault/lease_test.clj new file mode 100644 index 00000000..03127eba --- /dev/null +++ b/test-resources/lib_tests/vault/lease_test.clj @@ -0,0 +1,98 @@ +(ns vault.lease-test + (:require + [clojure.test :refer [deftest is]] + [vault.lease :as lease]) + (:import + java.time.Instant)) + + +(defmacro with-time + "Evaluates the given body of forms with `vault.lease/now` rebound to always + give the result `t`." + [t & body] + `(with-redefs [vault.lease/now (constantly (Instant/ofEpochMilli ~t))] + ~@body)) + + +(deftest missing-info + (let [c (lease/new-store)] + (is (nil? (lease/lookup c :foo)) + "lookup of unstored key should return nil") + (is (nil? (lease/update! c nil)) + "storing nil should return nil") + (is (nil? (lease/lookup c :foo)) + "lookup of nil store should return nil"))) + + +(deftest secret-expiry + (let [c (lease/new-store)] + (with-time 1000 + (is (= {:path "foo/bar" + :data {:bar "baz"} + :lease-id "foo/bar/12345" + :lease-duration 100 + :renewable true + :vault.lease/issued (Instant/ofEpochMilli 1000) + :vault.lease/expiry (Instant/ofEpochMilli 101000)} + (lease/update! c {:path "foo/bar" + :lease-id "foo/bar/12345" + :lease-duration 100 + :renewable true + :data {:bar "baz"}})) + "storing secret info should return data structure")) + (with-time 50000 + (is (= {:path "foo/bar" + :data {:bar "baz"} + :lease-id "foo/bar/12345" + :lease-duration 100 + :renewable true + :vault.lease/issued (Instant/ofEpochMilli 1000) + :vault.lease/expiry (Instant/ofEpochMilli 101000)} + (lease/lookup c "foo/bar")) + "lookup of stored secret within expiry should return data structure")) + (with-time 101001 + (is (lease/expired? (lease/lookup c "foo/bar")) + "lookup of stored secret after expiry should return nil")))) + + +(deftest lease-filtering + (let [c (lease/new-store) + the-lease {:path "foo/bar" + :lease-id "foo/bar/12345" + :lease-duration 100 + :renewable true + :vault.lease/renew true + :vault.lease/rotate true + :vault.lease/issued (Instant/ofEpochMilli 1000) + :vault.lease/expiry (Instant/ofEpochMilli 101000)}] + (with-time 1000 + (lease/update! c {:path "foo/bar" + :data {:bar "baz"} + :lease-id "foo/bar/12345" + :lease-duration 100 + :renewable true + :renew true + :rotate true})) + (with-time 101001 + (is (= [the-lease] (lease/list-leases c)) + "Basic lease listing should work, and the data should match.") + (is (= [the-lease] (lease/rotatable-leases c 0)) + "Expired but rotatable lease should be considered rotatable")) + (with-time 100000 + (is (= [the-lease] (lease/renewable-leases c 2)), + "Renewable leases should be listed when not expired yet.") + (is (empty? (lease/renewable-leases c 1)), + "Renewable leases should not be listed when outside the given window.") + (is (empty? (lease/rotatable-leases c 0)) + "Non-expired, renewable leases should not be considered for rotation.")))) + + +(deftest secret-invalidation + (let [c (lease/new-store)] + (is (some? (lease/update! c {:path "foo/bar" + :data {:baz "qux"} + :lease-id "foo/bar/12345"}))) + (is (some? (lease/lookup c "foo/bar"))) + (is (nil? (lease/remove-path! c "foo/bar"))) + (is (nil? (lease/lookup c "foo/bar")) + "lookup of invalidated secret should return nil"))) diff --git a/test-resources/lib_tests/vault/secrets/kvv1_test.clj b/test-resources/lib_tests/vault/secrets/kvv1_test.clj new file mode 100644 index 00000000..0826da42 --- /dev/null +++ b/test-resources/lib_tests/vault/secrets/kvv1_test.clj @@ -0,0 +1,183 @@ +(ns vault.secrets.kvv1-test + (:require + [cheshire.core :as json] + [clojure.test :refer [is testing deftest]] + [org.httpkit.client :as http] + [vault.client.http :as http-client] + [vault.client.mock-test :as mock-test] + [vault.core :as vault] + [vault.secrets.kvv1 :as vault-kvv1]) + (:import + (clojure.lang + ExceptionInfo))) + + +;; -------- HTTP Client ------------------------------------------------------- + +(deftest list-secrets-test + (let [path "path/passed/in" + token-passed-in "fake-token" + vault-url "https://vault.example.amperity.com" + client (http-client/http-client vault-url) + response {:auth nil + :data {:keys ["foo" "foo/"]} + :lease_duration 2764800 + :lease-id "" + :renewable false}] + (vault/authenticate! client :token token-passed-in) + (testing "List secrets has correct response and sends correct request" + (with-redefs + [http/request + (fn [req] + (is (= :get (:method req))) + (is (= (str vault-url "/v1/" path) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (true? (-> req :query-params :list))) + (atom {:body response}))] + (is (= ["foo" "foo/"] + (vault-kvv1/list-secrets client path))))))) + + +(deftest read-secret-test + (let [lookup-response-valid-path (json/generate-string {:auth nil + :data {:foo "bar" + :ttl "1h"} + :lease_duration 3600 + :lease_id "" + :renewable false}) + path-passed-in "path/passed/in" + path-passed-in2 "path/passed/in2" + token-passed-in "fake-token" + vault-url "https://vault.example.amperity.com" + client (http-client/http-client vault-url)] + (vault/authenticate! client :token token-passed-in) + (testing "Read secrets sends correct request and responds correctly if secret is successfully located" + (with-redefs + [http/request + (fn [req] + (is (= :get (:method req))) + (is (= (str vault-url "/v1/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (atom {:body lookup-response-valid-path}))] + (is (= {:foo "bar" :ttl "1h"} (vault-kvv1/read-secret client path-passed-in))))) + (testing "Read secrets sends correct request and responds correctly if no secret is found" + (with-redefs + [http/request + (fn [req] + (is (= (str vault-url "/v1/" path-passed-in2) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (throw (ex-info "not found" {:error [] :status 404})))] + (try + (vault-kvv1/read-secret client path-passed-in2) + (catch ExceptionInfo e + (is (= {:errors nil + :status 404 + :type :vault.client.api-util/api-error} + (ex-data e))))))))) + + +(deftest write-secret-test + (let [create-success (json/generate-string {:data {:created_time "2018-03-22T02:24:06.945319214Z" + :deletion_time "" + :destroyed false + :version 1}}) + write-data {:foo "bar" + :zip "zap"} + path-passed-in "path/passed/in" + token-passed-in "fake-token" + vault-url "https://vault.example.amperity.com" + client (http-client/http-client vault-url)] + (vault/authenticate! client :token token-passed-in) + (testing "Write secrets sends correct request and returns true upon success" + (with-redefs + [http/request + (fn [req] + (is (= :post (:method req))) + (is (= (str vault-url "/v1/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (= write-data (:form-params req))) + (atom {:body create-success + :status 204}))] + (is (true? (vault-kvv1/write-secret! client path-passed-in write-data))))) + (testing "Write secrets sends correct request and returns false upon failure" + (with-redefs + [http/request + (fn [req] + (is (= :post (:method req))) + (is (= (str vault-url "/v1/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (= write-data + (:form-params req))) + (atom {:errors [] + :status 400}))] + (is (false? (vault-kvv1/write-secret! client path-passed-in write-data))))))) + + +(deftest delete-secret-test + (let [path-passed-in "path/passed/in" + token-passed-in "fake-token" + vault-url "https://vault.example.amperity.com" + client (http-client/http-client vault-url)] + (vault/authenticate! client :token "fake-token") + (testing "Delete secret returns correctly upon success, and sends correct request" + (with-redefs + [http/request + (fn [req] + (is (= :delete (:method req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (= (str vault-url "/v1/" path-passed-in) (:url req))) + (atom {:status 204}))] + (is (true? (vault/delete-secret! client path-passed-in))))) + (testing "Delete secret returns correctly upon failure, and sends correct request" + (with-redefs + [http/request + (fn [req] + (is (= :delete (:method req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (= (str vault-url "/v1/" path-passed-in) (:url req))) + (atom {:status 404}))] + (is (false? (vault/delete-secret! client path-passed-in))))))) + + +;; -------- Mock Client ------------------------------------------------------- + +(deftest mock-client-test + (testing "Mock client can correctly read values it was initialized with" + (is (= {:batman "Bruce Wayne" + :captain-marvel "Carol Danvers"} + (vault-kvv1/read-secret (mock-test/mock-client-authenticated) "identities")))) + (testing "Mock client correctly responds with a 404 to non-existent paths" + (is (thrown-with-msg? ExceptionInfo #"No such secret: hello" + (vault-kvv1/read-secret (mock-test/mock-client-authenticated) "hello"))) + (is (thrown-with-msg? ExceptionInfo #"No such secret: identities" + (vault-kvv1/read-secret (vault/new-client "mock:-") "identities")))) + (testing "Mock client can write/update and read data" + (let [client (mock-test/mock-client-authenticated)] + (is (thrown-with-msg? ExceptionInfo #"No such secret: hello" + (vault-kvv1/read-secret client "hello"))) + (is (true? (vault-kvv1/write-secret! client "hello" {:and-i-say "goodbye"}))) + (is (true? (vault-kvv1/write-secret! client "identities" {:intersect "Chuck"}))) + (is (= {:and-i-say "goodbye"} + (vault-kvv1/read-secret client "hello"))) + (is (= {:intersect "Chuck"} + (vault-kvv1/read-secret client "identities"))))) + (testing "Mock client can list secrets" + (let [client (mock-test/mock-client-authenticated)] + (is (empty? (vault-kvv1/list-secrets client "hello"))) + (is (true? (vault-kvv1/write-secret! client "hello" {:and-i-say "goodbye"}))) + (is (true? (vault-kvv1/write-secret! client "identities" {:intersect "Chuck"}))) + (is (= ["identities" "hello"] (into [] (vault-kvv1/list-secrets client "")))) + (is (= ["identities"] (into [] (vault-kvv1/list-secrets client "identities")))))) + (testing "Mock client can delete secrets" + (let [client (mock-test/mock-client-authenticated)] + (is (true? (vault-kvv1/write-secret! client "hello" {:and-i-say "goodbye"}))) + (is (= {:and-i-say "goodbye"} + (vault-kvv1/read-secret client "hello"))) + (is (= {:batman "Bruce Wayne" + :captain-marvel "Carol Danvers"} + (vault-kvv1/read-secret client "identities"))) + ;; delete them + (is (true? (vault-kvv1/delete-secret! client "hello"))) + (is (true? (vault-kvv1/delete-secret! client "identities"))) + (is (thrown? ExceptionInfo (vault-kvv1/read-secret client "hello"))) + (is (thrown? ExceptionInfo (vault-kvv1/read-secret client "identities")))))) diff --git a/test-resources/lib_tests/vault/secrets/kvv2_test.clj b/test-resources/lib_tests/vault/secrets/kvv2_test.clj new file mode 100644 index 00000000..cfd005e3 --- /dev/null +++ b/test-resources/lib_tests/vault/secrets/kvv2_test.clj @@ -0,0 +1,493 @@ +(ns vault.secrets.kvv2-test + (:require + [cheshire.core :as json] + [clojure.test :refer [testing deftest is]] + [org.httpkit.client :as http] + [vault.client.api-util :as api-util] + [vault.client.http :as http-client] + [vault.client.mock-test :as mock-test] + [vault.core :as vault] + [vault.secrets.kvv2 :as vault-kvv2]) + (:import + (clojure.lang + ExceptionInfo))) + + +(deftest list-secrets-test + (let [path "path/passed/in" + token-passed-in "fake-token" + vault-url "https://vault.example.amperity.com" + client (http-client/http-client vault-url) + response {:auth nil + :data {:keys ["foo" "foo/"]} + :lease_duration 2764800 + :lease_id "" + :renewable false}] + (vault/authenticate! client :token token-passed-in) + (testing "List secrets has correct response and sends correct request" + (with-redefs + [http/request + (fn [req] + (is (= :get (:method req))) + (is (= (str vault-url "/v1/listmount/metadata/" path) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (true? (-> req :query-params :list))) + (atom {:body response}))] + (is (= ["foo" "foo/"] + (vault-kvv2/list-secrets client "listmount" path))))))) + + +(deftest write-config!-test + (let [mount "mount" + token-passed-in "fake-token" + vault-url "https://vault.example.amperity.com" + client (http-client/http-client vault-url) + new-config-kebab {:max-versions 5 + :cas-required false + :delete-version-after "3h25m19s"} + new-config-snake {:max_versions 5 + :cas_required false + :delete_version_after "3h25m19s"}] + (vault/authenticate! client :token token-passed-in) + (testing "Write config sends correct request and returns true on valid call" + (with-redefs + [http/request + (fn [req] + (is (= :post (:method req))) + (is (= (str vault-url "/v1/" mount "/config") (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (= new-config-snake (:form-params req))) + (atom {:status 204}))] + (is (true? (vault-kvv2/write-config! client mount new-config-kebab))))))) + + +(deftest read-config-test + (let [config {:max-versions 5 + :cas-required false + :delete-version-after "3h25m19s"} + body-str (json/generate-string {:data (api-util/snakeify-keys config)}) + mount "mount" + token-passed-in "fake-token" + vault-url "https://vault.example.amperity.com" + client (http-client/http-client vault-url)] + (vault/authenticate! client :token token-passed-in) + (testing "Read config sends correct request and returns the config with valid call" + (with-redefs + [http/request + (fn [req] + (is (= :get (:method req))) + (is (= (str vault-url "/v1/" mount "/config") (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (atom {:body body-str}))] + (is (= config (vault-kvv2/read-config client mount))))))) + + +(deftest read-test + (let [lookup-response-valid-path (json/generate-string {:data {:data {:foo "bar"} + :metadata {:created_time "2018-03-22T02:24:06.945319214Z" + :deletion_time "" + :destroyed false + :version 1}}}) + mount "mount" + path-passed-in "path/passed/in" + token-passed-in "fake-token" + vault-url "https://vault.example.amperity.com" + client (http-client/http-client vault-url)] + (vault/authenticate! client :token token-passed-in) + (testing "Read secrets sends correct request and responds correctly if secret is successfully located" + (with-redefs + [http/request + (fn [req] + (is (= :get (:method req))) + (is (= (str vault-url "/v1/" mount "/data/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (atom {:body lookup-response-valid-path}))] + (is (= {:foo "bar"} (vault-kvv2/read-secret client mount path-passed-in))))) + (testing "Read secrets sends correct request and responds correctly if secret with version is successfully located" + (with-redefs + [http/request + (fn [req] + (is (= :get (:method req))) + (is (= (str vault-url "/v1/" mount "/data/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (= {"version" 3} (:query-params req))) + (atom {:body lookup-response-valid-path}))] + (is (= {:foo "bar"} (vault-kvv2/read-secret client mount path-passed-in {:version 3 :force-read true}))))) + (testing "Read secrets sends correct request and responds correctly if no secret is found" + (with-redefs + [http/request + (fn [req] + (is (= :get (:method req))) + (is (= (str vault-url "/v1/" mount "/data/different/path") (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (throw (ex-info "not found" {:errors [] :status 404 :type :vault.client.api-util/api-error})))] + (try + (is (= {:default-val :is-here} + (vault-kvv2/read-secret + client + mount + "different/path" + {:not-found {:default-val :is-here}}))) + + (vault-kvv2/read-secret client mount "different/path") + (is false) + (catch ExceptionInfo e + (is (= {:errors nil + :status 404 + :type ::api-util/api-error} + (ex-data e))))))))) + + +(deftest write!-test + (let [create-success {:data {:created_time "2018-03-22T02:24:06.945319214Z" + :deletion_time "" + :destroyed false + :version 1}} + write-data {:foo "bar" + :zip "zap"} + mount "mount" + path-passed-in "path/passed/in" + token-passed-in "fake-token" + vault-url "https://vault.example.amperity.com" + client (http-client/http-client vault-url)] + (vault/authenticate! client :token token-passed-in) + (testing "Write secrets sends correct request and returns true upon success" + (with-redefs + [http/request + (fn [req] + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (= :post (:method req))) + (if (= (str vault-url "/v1/" mount "/metadata/" path-passed-in) (:url req)) + (do (is (= {} (:form-params req))) + (atom {:errors [] + :status 200})) + (do (is (= (str vault-url "/v1/" mount "/data/" path-passed-in) (:url req))) + (is (= {:data write-data} + (:form-params req))) + (atom {:body create-success + :status 200}))))] + (is (= (:data create-success) (vault-kvv2/write-secret! client mount path-passed-in write-data))))) + (testing "Write secrets sends correct request and returns false upon failure" + (with-redefs + [http/request + (fn [req] + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (= :post (:method req))) + (if (= (str vault-url "/v1/" mount "/metadata/other-path") (:url req)) + (do (is (= {} (:form-params req))) + (atom {:errors [] + :status 200})) + (do (is (= (str vault-url "/v1/" mount "/data/other-path") (:url req))) + (is (= {:data write-data} + (:form-params req))) + (atom {:errors [] + :status 500}))))] + (is (false? (vault-kvv2/write-secret! client mount "other-path" write-data))))))) + + +(deftest delete-test + (let [mount "mount" + path-passed-in "path/passed/in" + token-passed-in "fake-token" + vault-url "https://vault.example.amperity.com" + client (http-client/http-client vault-url)] + (vault/authenticate! client :token token-passed-in) + (testing "delete secrets send correct request and returns true upon success when no versions passed in" + (with-redefs + [http/request + (fn [req] + (is (= :delete (:method req))) + (is (= (str vault-url "/v1/" mount "/data/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (atom {:status 204}))] + (is (true? (vault-kvv2/delete-secret! client mount path-passed-in)) + (is (true? (vault-kvv2/delete-secret! client mount path-passed-in [])))) + (testing "delete secrets send correct request and returns false upon failure when no versions passed in" + (with-redefs + [http/request + (fn [req] + (is (= :delete (:method req))) + (is (= (str vault-url "/v1/" mount "/data/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (atom {:status 404}))] + (is (false? (vault-kvv2/delete-secret! client mount path-passed-in))))) + (testing "delete secrets send correct request and returns true upon success when multiple versions passed in" + (with-redefs + [http/request + (fn [req] + (is (= :post (:method req))) + (is (= (str vault-url "/v1/" mount "/delete/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (= {:versions [12 14 147]} (:form-params req))) + (atom {:status 204}))] + (is (true? (vault-kvv2/delete-secret! client mount path-passed-in [12 14 147]))))) + (testing "delete secrets send correct request and returns false upon failure when multiple versions passed in" + (with-redefs + [http/request + (fn [req] + (is (= :post (:method req))) + (is (= (str vault-url "/v1/" mount "/delete/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (= {:versions [123]} (:form-params req))) + (atom {:status 404}))] + (is (false? (vault-kvv2/delete-secret! client mount path-passed-in [123]))))))))) + + +(deftest read-metadata-test + (let [data (json/generate-string {:data + {:created_time "2018-03-22T02:24:06.945319214Z" + :current_version 3 + :max_versions 0 + :oldest_version 0 + :updated_time "2018-03-22T02:36:43.986212308Z" + :versions {:1 {:created_time "2018-03-22T02:24:06.945319214Z" + :deletion_time "" + :destroyed false} + :2 {:created_time "2018-03-22T02:36:33.954880664Z" + :deletion_time "" + :destroyed false} + :3 {:created_time "2018-03-22T02:36:43.986212308Z" + :deletion_time "" + :destroyed false}}}}) + kebab-metadata (json/generate-string {:created-time "2018-03-22T02:24:06.945319214Z" + :current-version 3 + :max-versions 0 + :oldest-version 0 + :updated-time "2018-03-22T02:36:43.986212308Z" + :versions {:1 {:created-time "2018-03-22T02:24:06.945319214Z" + :deletion-time "" + :destroyed false} + :2 {:created-time "2018-03-22T02:36:33.954880664Z" + :deletion-time "" + :destroyed false} + :3 {:created-time "2018-03-22T02:36:43.986212308Z" + :deletion-time "" + :destroyed false}}}) + mount "mount" + path-passed-in "path/passed/in" + token-passed-in "fake-token" + vault-url "https://vault.example.amperity.com" + client (http-client/http-client vault-url)] + (vault/authenticate! client :token token-passed-in) + (testing "Sends correct request and responds correctly upon success" + (with-redefs + [http/request + (fn [req] + (is (= :get (:method req))) + (is (= (str vault-url "/v1/" mount "/metadata/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (atom {:body data + :status 200}))] + (is (= kebab-metadata (json/generate-string (vault-kvv2/read-metadata client mount path-passed-in)))))) + (testing "Sends correct request and responds correctly when metadata not found" + (with-redefs + [http/request + (fn [req] + (is (= :get (:method req))) + (is (= (str vault-url "/v1/" mount "/metadata/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (throw (ex-info "not found" {:errors [] :status 404 :type :vault.client.api-util/api-error})))] + (is (thrown? ExceptionInfo (vault-kvv2/read-metadata client mount path-passed-in {:force-read true}))) + (is (= 3 (vault-kvv2/read-metadata client mount path-passed-in {:not-found 3 + :force-read true}))))))) + + +(deftest write-metadata-test + (let [payload {:max-versions 5, + :cas-required false, + :delete-version-after "3h25m19s"} + mount "mount" + path-passed-in "path/passed/in" + token-passed-in "fake-token" + vault-url "https://vault.example.amperity.com" + client (http-client/http-client vault-url)] + (vault/authenticate! client :token token-passed-in) + (testing "Write metadata sends correct request and responds with true upon success" + (with-redefs + [http/request + (fn [req] + (is (= :post (:method req))) + (is (= (str vault-url "/v1/" mount "/metadata/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (= (api-util/snakeify-keys payload) (:form-params req))) + (atom {:status 204}))] + (is (true? (vault-kvv2/write-metadata! client mount path-passed-in payload))))) + (testing "Write metadata sends correct request and responds with false upon failure" + (with-redefs + [http/request + (fn [req] + (is (= :post (:method req))) + (is (= (str vault-url "/v1/" mount "/metadata/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (= (api-util/snakeify-keys payload) (:form-params req))) + (atom {:status 500}))] + (is (false? (vault-kvv2/write-metadata! client mount path-passed-in payload))))))) + + +(deftest delete-metadata-test + (let [mount "mount" + path-passed-in "path/passed/in" + token-passed-in "fake-token" + vault-url "https://vault.example.amperity.com" + client (http-client/http-client vault-url)] + (vault/authenticate! client :token token-passed-in) + (testing "Sends correct request and responds correctly upon success" + (with-redefs + [http/request + (fn [req] + (is (= :delete (:method req))) + (is (= (str vault-url "/v1/" mount "/metadata/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (atom {:status 204}))] + (is (true? (vault-kvv2/delete-metadata! client mount path-passed-in))))) + (testing "Sends correct request and responds correctly upon failure" + (with-redefs + [http/request + (fn [req] + (is (= :delete (:method req))) + (is (= (str vault-url "/v1/" mount "/metadata/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (atom {:status 500}))] + (is (false? (vault-kvv2/delete-metadata! client mount path-passed-in))))))) + + +(deftest destroy!-test + (let [mount "mount" + path-passed-in "path/passed/in" + token-passed-in "fake-token" + vault-url "https://vault.example.amperity.com" + client (http-client/http-client vault-url) + versions [1 2]] + (vault/authenticate! client :token token-passed-in) + (testing "Destroy secrets sends correct request and returns true upon success" + (with-redefs + [http/request + (fn [req] + (is (= :post (:method req))) + (is (= (str vault-url "/v1/" mount "/destroy/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (= {:versions versions} + (:form-params req))) + (atom {:status 204}))] + (is (true? (vault-kvv2/destroy-secret! client mount path-passed-in versions))))) + (testing "Destroy secrets sends correct request and returns false upon failure" + (with-redefs + [http/request + (fn [req] + (is (= :post (:method req))) + (is (= (str vault-url "/v1/" mount "/destroy/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (= {:versions [1]} + (:form-params req))) + (atom {:status 500}))] + (is (false? (vault-kvv2/destroy-secret! client mount path-passed-in [1]))))))) + + +(deftest undelete-secret!-test + (let [mount "mount" + path-passed-in "path/passed/in" + token-passed-in "fake-token" + vault-url "https://vault.example.amperity.com" + client (http-client/http-client vault-url) + versions [1 2]] + (vault/authenticate! client :token token-passed-in) + (testing "Undelete secrets sends correct request and returns true upon success" + (with-redefs + [http/request + (fn [req] + (is (= :post (:method req))) + (is (= (str vault-url "/v1/" mount "/undelete/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (= {:versions versions} + (:form-params req))) + (atom {:status 204}))] + (is (true? (vault-kvv2/undelete-secret! client mount path-passed-in versions))))) + (testing "Undelete secrets sends correct request and returns false upon failure" + (with-redefs + [http/request + (fn [req] + (is (= :post (:method req))) + (is (= (str vault-url "/v1/" mount "/undelete/" path-passed-in) (:url req))) + (is (= token-passed-in (get (:headers req) "X-Vault-Token"))) + (is (= {:versions [1]} + (:form-params req))) + (atom {:status 500}))] + (is (false? (vault-kvv2/undelete-secret! client mount path-passed-in [1]))))))) + + +;; -------- Mock Client ------------------------------------------------------- + +(defn mock-client-kvv2 + "Creates a mock client with the data in `vault/secrets/secret-fixture-kvv2.edn`" + [] + (mock-test/mock-client-authenticated "vault/secrets/secret-fixture-kvv2.edn")) + + +(deftest mock-client-test + (testing "Mock client can correctly read values it was initialized with" + (is (= {:batman "Bruce Wayne" + :captain-marvel "Carol Danvers"} + (vault-kvv2/read-secret (mock-client-kvv2) "mount" "identities")))) + (testing "Mock client correctly responds with a 404 to reading non-existent paths" + (is (thrown-with-msg? ExceptionInfo #"No such secret: mount/data/hello" + (vault-kvv2/read-secret (mock-client-kvv2) "mount" "hello"))) + (is (thrown-with-msg? ExceptionInfo #"No such secret: mount/data/identities" + (vault-kvv2/read-secret (vault/new-client "mock:-") "mount" "identities")))) + (testing "Mock client can write/update and read data" + (let [client (mock-client-kvv2)] + (is (thrown-with-msg? ExceptionInfo #"No such secret: mount/data/hello" + (vault-kvv2/read-secret client "mount" "hello"))) + (is (true? (vault-kvv2/write-secret! client "mount" "hello" {:and-i-say "goodbye"}))) + (is (true? (vault-kvv2/write-secret! client "mount" "identities" {:intersect "Chuck"}))) + (is (= {:and-i-say "goodbye"} + (vault-kvv2/read-secret client "mount" "hello"))) + (is (= {:intersect "Chuck"} + (vault-kvv2/read-secret client "mount" "identities"))))) + (testing "Mock client can write and read config" + (let [client (mock-client-kvv2) + config {:max-versions 5 + :cas-required false + :delete-version-after "3h23m19s"}] + (is (thrown? ExceptionInfo + (vault-kvv2/read-config client "mount"))) + (is (true? (vault-kvv2/write-config! client "mount" config))) + (is (= config (vault-kvv2/read-config client "mount"))))) + (testing "Mock client can write and read metadata" + (let [client (mock-client-kvv2)] + (is (thrown? ExceptionInfo + (vault-kvv2/read-metadata client "mount" "doesn't exist" {:force-read true}))) + (is (= {:created-time "2018-03-22T02:24:06.945319214Z" + :current-version 1 + :max-versions 0 + :oldest-version 0 + :updated-time "2018-03-22T02:36:43.986212308Z" + :versions {:1 {:created-time "2018-03-22T02:24:06.945319214Z" + :deletion-time "" + :destroyed false}}} + (vault-kvv2/read-metadata client "mount" "identities" {:force-read true}))) + (is (true? (vault-kvv2/delete-metadata! client "mount" "identities"))) + (is (thrown? ExceptionInfo + (vault-kvv2/read-metadata client "mount" "identities" {:force-read true}))) + (is (true? (vault-kvv2/write-metadata! client "mount" "hello" {:max-versions 3}))) + (is (= 3 (:max-versions (vault-kvv2/read-metadata client "mount" "hello")))) + (is (= 5 (vault-kvv2/read-metadata client "mount" "doesn't exist" {:force-read true + :not-found 5}))))) + (testing "Mock client returns true if path is found on delete for secret, false if not when no versions specified" + (let [client (mock-client-kvv2)] + (is (true? (vault-kvv2/delete-secret! client "mount" "identities"))) + (is (false? (vault-kvv2/delete-secret! client "mount" "eggsactly"))))) + (testing "Mock client always returns true on delete for secret when versions specified" + (let [client (mock-client-kvv2)] + (is (true? (vault-kvv2/delete-secret! client "mount" "identities" [1]))) + (is (true? (vault-kvv2/delete-secret! client "mount" "eggsactly" [4 5 6]))))) + (testing "Mock can list secrets from their associated metadata" + (let [client (mock-client-kvv2)] + (is (empty? (vault-kvv2/list-secrets client "hello" "yes"))) + (is (true? (vault-kvv2/write-secret! client "mount" "hello" {:and-i-say "goodbye"}))) + ;; Paths are good enough for mock right now, but be aware they are current + (is (= ["identities" "hello"] + (into [] (vault-kvv2/list-secrets client "mount" "")))))) + (testing "Mock client does not crash upon destroy" + (is (true? (vault-kvv2/destroy-secret! (mock-client-kvv2) "mount" "identities" [1])))) + (testing "Mock client does not crash upon undelete" + (is (true? (vault-kvv2/undelete-secret! (mock-client-kvv2) "mount" "identities" [1]))))) diff --git a/test-resources/lib_tests/vault/secrets/secret-fixture-kvv2.edn b/test-resources/lib_tests/vault/secrets/secret-fixture-kvv2.edn new file mode 100644 index 00000000..b66d49e8 --- /dev/null +++ b/test-resources/lib_tests/vault/secrets/secret-fixture-kvv2.edn @@ -0,0 +1,12 @@ +{"mount/data/identities" + {:data {:batman "Bruce Wayne" + :captain-marvel "Carol Danvers"}} + "mount/metadata/identities" + {:created_time "2018-03-22T02:24:06.945319214Z" + :current_version 1 + :max_versions 0 + :oldest_version 0 + :updated_time "2018-03-22T02:36:43.986212308Z" + :versions {:1 {:created_time "2018-03-22T02:24:06.945319214Z" + :deletion_time "" + :destroyed false}}}}