├── .gitignore ├── src └── api_live_tests │ ├── core.clj │ ├── flowdock.clj │ ├── app_utils.clj │ ├── api.clj │ └── generators.clj ├── project.clj └── test └── api_live_tests └── core_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | .env -------------------------------------------------------------------------------- /src/api_live_tests/core.clj: -------------------------------------------------------------------------------- 1 | (ns api-live-tests.core 2 | (:gen-class) 3 | (:require [clj-time.core :as t] 4 | [api-live-tests.api :as api])) 5 | 6 | (defn install-via-api [app-id installation-name] 7 | (let [installation-id (api/install-non-reqs-app app-id installation-name) 8 | _ (println "installed app") 9 | _ (api/uninstall-app installation-id)] 10 | (println "uninstalled app"))) 11 | 12 | (defn -main [& args] 13 | (install-via-api 6183 "Installation name")) 14 | -------------------------------------------------------------------------------- /src/api_live_tests/flowdock.clj: -------------------------------------------------------------------------------- 1 | (ns api-live-tests.flowdock 2 | (:require [clj-http.client :as client])) 3 | 4 | (def fd-token (System/getenv "FD_TOKEN")) 5 | (defn post-fd-message [message] 6 | (client/post (str "https://api.flowdock.com/v1/messages/chat/" fd-token) 7 | {:form-params {:content message 8 | :external_user_name "Jenkins"} 9 | :content-type :json})) 10 | 11 | (defn -main [message] 12 | (post-fd-message message)) -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject api-live-tests "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :url "http://example.com/FIXME" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :main api-live-tests.core 7 | :injections [(require 'spyscope.core)] 8 | :dependencies [[org.clojure/clojure "1.7.0"] 9 | [prismatic/plumbing "0.4.4"] 10 | [w01fe/sniper "0.1.0"] 11 | [org.clojure/data.csv "0.1.3"] 12 | [clj-http "2.0.0"] 13 | [clj-statsd "0.3.11"] 14 | [org.clojure/test.check "0.7.0"] 15 | [com.velisco/herbert "0.6.11"] 16 | [com.gfredericks/test.chuck "0.1.21"] 17 | [spyscope "0.1.5"] 18 | [org.clojure/tools.trace "0.7.8"] 19 | [backtick "0.3.3"] 20 | [debugger "0.1.7"] 21 | [clj-zendesk "0.3.0-SNAPSHOT"] 22 | [clj-time "0.10.0"]]) 23 | -------------------------------------------------------------------------------- /test/api_live_tests/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns api-live-tests.core-test 2 | (:require [clojure.test :refer :all] 3 | [api-live-tests.core :refer :all] 4 | [api-live-tests.generators :refer [app-gen generate-app journey-gen journey-gen-two]] 5 | [clojure.test.check.properties :as prop] 6 | [clojure.pprint :refer [pprint]] 7 | [clojure.test.check.clojure-test :refer [defspec]] 8 | [api-live-tests.api :as api :refer [upload-and-create-app delete-app 9 | install-app get-installations 10 | get-owned-apps 11 | destroy-all-apps 12 | uninstall-app]] 13 | [clojure.test.check.generators :as gen] 14 | [clojure.tools.trace :refer [trace-ns]])) 15 | 16 | (def number-of-journeys-to-take 17 | (try 18 | (Integer. (System/getenv "NUMBER_OF_JOURNEYS_TO_TAKE")) 19 | (catch NumberFormatException _ 20 | 1))) 21 | 22 | (def seed 23 | (try 24 | (Integer. (System/getenv "SEED")) 25 | (catch NumberFormatException _ 26 | nil))) 27 | 28 | (trace-ns 'api-live-tests.api) 29 | (trace-ns 'api-live-tests.core-test) 30 | 31 | (defn assert-world-state-is-correct [state] 32 | (let [expected-apps (:apps state) 33 | real-apps (get-owned-apps) 34 | expected-installations (:installations state) 35 | real-installations (get-installations)] 36 | (is (= (count real-apps) (count expected-apps))) 37 | (is (= (count real-installations) (count expected-installations))) 38 | (println "Number of apps and installations is as expected!") 39 | (println))) 40 | 41 | (defn journey-can-be-completed? [journey] 42 | (destroy-all-apps) 43 | 44 | (pprint "Undertaking journey:") 45 | (pprint (map (comp :name first) journey)) 46 | (println) 47 | 48 | (reduce (fn [state [action thing]] 49 | (println (str "doing step…" (:name action))) 50 | 51 | (let [{:keys [transform perform]} action 52 | expected-new-state (perform state (transform state thing) thing)] 53 | (doto expected-new-state 54 | assert-world-state-is-correct))) 55 | {} 56 | journey)) 57 | 58 | (defspec apps-can-be-installed 59 | { :num-tests number-of-journeys-to-take :seed seed } 60 | 61 | (prop/for-all [journey journey-gen] 62 | (journey-can-be-completed? journey))) 63 | -------------------------------------------------------------------------------- /src/api_live_tests/app_utils.clj: -------------------------------------------------------------------------------- 1 | (ns api-live-tests.app-utils 2 | (:require [api-live-tests.api :as api :refer [upload-and-create-app destroy-all-apps install-app]] 3 | [clojure.java.io :refer [file make-parents]] 4 | [clojure.java.shell :refer [sh]] 5 | [cheshire.core :refer [parse-string generate-string]])) 6 | 7 | (defn mk-tmp-dir! 8 | "Creates a unique temporary directory on the filesystem. Typically in /tmp on 9 | *NIX systems. Returns a File object pointing to the new directory. Raises an 10 | exception if the directory couldn't be created after 10000 tries. 11 | 12 | https://gist.github.com/samaaron/1398198 13 | " 14 | [] 15 | (let [base-dir (file (System/getProperty "java.io.tmpdir")) 16 | base-name (str (System/currentTimeMillis) "-" (long (rand 1000000000)) "-") 17 | tmp-base (str (if (.endsWith (.getPath base-dir) "/") 18 | (.getPath base-dir) 19 | (str (.getPath base-dir) "/")) 20 | base-name) 21 | max-attempts 10000] 22 | (loop [num-attempts 1] 23 | (if (= num-attempts max-attempts) 24 | (throw (Exception. (str "Failed to create temporary directory after " max-attempts " attempts."))) 25 | (let [tmp-dir-name (str tmp-base num-attempts) 26 | tmp-dir (file tmp-dir-name)] 27 | (if (.mkdir tmp-dir) 28 | tmp-dir 29 | (recur (inc num-attempts)))))))) 30 | 31 | (defn hyphens-to-camel-case-name 32 | "e.g. hello-world -> helloWorld" 33 | [method-name] 34 | (clojure.string/replace method-name #"-(\w)" 35 | #(clojure.string/upper-case (second %1)))) 36 | 37 | (defn keys-to-camel-case [data] 38 | (if (map? data) 39 | (into {} 40 | (for [[k v] data] 41 | [(hyphens-to-camel-case-name (name k)) (keys-to-camel-case v)])) 42 | data)) 43 | 44 | (defn serialize-app-to-tmpdir! [{:keys [translations manifest requirements app-js] :as app}] 45 | (let [dir (mk-tmp-dir!) 46 | translation-files (map #(file dir (str "translations/" % ".json")) translations) 47 | filenames-to-data {"manifest.json" (keys-to-camel-case manifest) 48 | "requirements.json" requirements}] 49 | (doseq [[filename data] filenames-to-data] 50 | (spit (file dir filename) 51 | (generate-string data 52 | {:pretty true}))) 53 | (doseq [translation-file translation-files] 54 | (make-parents translation-file) 55 | (spit translation-file (generate-string {}))) 56 | (when app-js (.createNewFile (file dir "app.js"))) 57 | dir)) 58 | 59 | (defn zip [dir] 60 | (sh "zip" "-r" "app" "." :dir dir) 61 | (file dir "app.zip")) 62 | -------------------------------------------------------------------------------- /src/api_live_tests/api.clj: -------------------------------------------------------------------------------- 1 | (ns api-live-tests.api 2 | (:use [clj-zendesk.core]) 3 | (:require [clj-http.client :as client] 4 | [clojure.tools.trace :refer [trace-ns]])) 5 | 6 | (def token 7 | (System/getenv "API_TOKEN")) 8 | 9 | (def api-url 10 | (System/getenv "API_URL")) 11 | 12 | (defn apps-url 13 | [path] (str api-url "/apps" path)) 14 | ; 15 | (def auth-creds 16 | [(str (System/getenv "API_EMAIL") "/token") token]) 17 | 18 | (defn create-upload [app-zip-filename] 19 | (let [response (client/post (apps-url "/uploads.json") 20 | {:basic-auth auth-creds 21 | :as :json 22 | :multipart [{:name "uploaded_data" 23 | :content (clojure.java.io/file app-zip-filename)}]})] 24 | (get-in response [:body :id]))) 25 | 26 | (defn create-app [upload-id app-name] 27 | (let [response (client/post (str api-url "/apps.json") 28 | {:basic-auth auth-creds 29 | :form-params {:name app-name 30 | :short_description "a description" 31 | :upload_id upload-id} 32 | :content-type :json 33 | :as :json})] 34 | (get-in response [:body :job_id]))) 35 | 36 | (defn get-job-status [job-id] 37 | (let [response (client/get (apps-url (str "/job_statuses/" job-id ".json")) 38 | {:basic-auth auth-creds 39 | :as :json})] 40 | (:body response))) 41 | 42 | (defn get-installation-job-status [job-id] 43 | (let [response (client/get (apps-url (str "/installations/job_statuses/" job-id ".json")) 44 | {:basic-auth auth-creds 45 | :as :json})] 46 | (:body response))) 47 | 48 | (defn app-id-when-job-completed [job-id] 49 | (Thread/sleep 2000) 50 | (loop [job-status (get-job-status job-id)] 51 | (Thread/sleep 2000) 52 | (case (:status job-status) 53 | "completed" (:app_id job-status) 54 | "failed" (do 55 | (println "FAILURE FAILURE FAILURE") 56 | (println (str "Job failed: " (:message job-status))) 57 | (System/exit 1)) 58 | (recur (get-job-status job-id))))) 59 | 60 | (defn upload-and-create-app [app-zip-filename app-name] 61 | (let [upload-id (create-upload app-zip-filename) 62 | job-status-id (create-app upload-id app-name)] 63 | (app-id-when-job-completed job-status-id))) 64 | 65 | (defn installation-id-when-job-completed [job-id] 66 | (Thread/sleep 2000) 67 | (loop [job-status (get-installation-job-status job-id)] 68 | (Thread/sleep 2000) 69 | (case (:status job-status) 70 | "completed" (:installation_id job-status) 71 | "failed" (do 72 | (println "FAILURE FAILURE FAILURE") 73 | (println (str "Job failed: " (:message job-status))) 74 | (System/exit 1)) 75 | (recur (get-installation-job-status job-id))))) 76 | 77 | (defn start-app-install [installation] 78 | (let [{:keys [settings app-id enabled]} installation 79 | response (client/post (apps-url "/installations.json") 80 | {:basic-auth auth-creds 81 | :form-params {:settings settings 82 | :enabled enabled 83 | :app_id app-id} 84 | :content-type :json 85 | :as :json})] 86 | (-> response :body :pending_job_id))) 87 | 88 | (defn install-non-reqs-app [app-id installation-name] 89 | (let [response (client/post (apps-url "/installations.json") 90 | {:basic-auth auth-creds 91 | :form-params {:settings {:name installation-name} 92 | :app_id app-id} 93 | :content-type :json 94 | :as :json})] 95 | (-> response :body :id))) 96 | 97 | (defn install-app [installation] 98 | (let [job-id (start-app-install installation)] 99 | (installation-id-when-job-completed job-id))) 100 | 101 | (defn uninstall-app [installation-id] 102 | (client/delete (apps-url (str "/installations/" installation-id ".json")) 103 | {:basic-auth auth-creds 104 | :content-type :json 105 | :as :json})) 106 | 107 | (defn delete-app [app-id] 108 | (client/delete (apps-url (str "/" app-id ".json")) 109 | {:basic-auth auth-creds 110 | :content-type :json 111 | :as :json})) 112 | 113 | (defn get-owned-apps [] 114 | (let [response (client/get (apps-url (str "/owned.json")) 115 | {:basic-auth auth-creds 116 | :as :json})] 117 | (get-in response [:body :apps]))) 118 | 119 | (defn get-installations [] 120 | (let [response (client/get (apps-url (str "/installations.json")) 121 | {:basic-auth auth-creds 122 | :as :json})] 123 | (get-in response [:body :installations]))) 124 | 125 | (defn destroy-all-apps [] 126 | (doseq [app-id (map :id (get-owned-apps))] 127 | (delete-app app-id))) 128 | 129 | (defn destroy-all-ticket-fields [] 130 | (doseq [ticketfield (get-all TicketFields)] 131 | (when (and (not (:system-field-options ticketfield)) (:removable ticketfield)) (delete-one TicketField (:id ticketfield))))) 132 | 133 | (defn destroy-all-triggers [] 134 | (doseq [trigger (get-all Triggers)] 135 | (delete-one Trigger (:id trigger)))) 136 | 137 | (defn destroy-all-targets [] 138 | (dorun (pmap #(delete-one Target (:id %)) (get-all Targets)))) 139 | 140 | -------------------------------------------------------------------------------- /src/api_live_tests/generators.clj: -------------------------------------------------------------------------------- 1 | (ns api-live-tests.generators 2 | (:use [debugger.core]) 3 | (:require [clojure.test.check.generators :refer [sample] :as gen] 4 | [com.gfredericks.test.chuck.generators :as chuck-gen] 5 | [clojure.tools.trace] 6 | [api-live-tests.app-utils :refer [serialize-app-to-tmpdir! zip]] 7 | [api-live-tests.api :as api :refer [upload-and-create-app delete-app 8 | install-app get-installations 9 | get-owned-apps 10 | destroy-all-apps 11 | uninstall-app]] 12 | [miner.herbert.generators :as hg] 13 | [backtick :refer [template]] 14 | [clojure.pprint :refer [pprint]] 15 | [api-live-tests.app-utils :refer [zip serialize-app-to-tmpdir!]])) 16 | 17 | (def not-empty-string '(str #"[A-Za-z0-9][A-Za-z0-9 ]+")) 18 | 19 | (def locale-gen (hg/generator '(or "en" "jp" "de"))) 20 | (def author-gen 21 | (hg/generator (template {:name ~not-empty-string 22 | :email ~not-empty-string}))) 23 | 24 | (defn manifest-gen [requirements-only parameters] 25 | (chuck-gen/for [default-locale locale-gen 26 | author author-gen 27 | private gen/boolean 28 | no-template gen/boolean] 29 | {:requirements-only requirements-only 30 | :default-locale default-locale 31 | :location (if requirements-only 32 | nil 33 | ["nav_bar"]) 34 | :author author 35 | :private private 36 | :parameters parameters 37 | :no-template no-template 38 | :framework-version (if requirements-only 39 | nil 40 | "1.0")})) 41 | 42 | (def ticket-field-gen 43 | (chuck-gen/for [tag (chuck-gen/string-from-regex #"[A-Za-z0-9]{8,12}")] 44 | [tag {:type "checkbox" 45 | :tag tag 46 | :title tag}])) 47 | 48 | (def ticket-fields-gen 49 | (chuck-gen/for [ticket-fields (gen/vector ticket-field-gen)] 50 | (into {} ticket-fields))) 51 | 52 | (defn targets-gen [parameters] 53 | (let [params-to-interpolate (map (comp (partial format "{{settings.%s}}") :name) parameters)] 54 | (hg/generator (template 55 | {~not-empty-string {:type "email_target" 56 | :title ~not-empty-string 57 | :email "blah@hoo.com" 58 | :subject (or ~not-empty-string 59 | ~@params-to-interpolate)}})))) 60 | 61 | (defn triggers-gen [custom-field-identifiers] 62 | (let [field-pointers (map (partial str "custom_fields_") custom-field-identifiers) 63 | condition (if (seq field-pointers) 64 | (template (or {"field" "priority" 65 | "operator" "is" 66 | "value" "high"} 67 | {"field" (or ~@field-pointers) 68 | "operator" "is" 69 | "value" (or "true" "false")})) 70 | {"field" "status" 71 | "operator" "is" 72 | "value" "open"})] 73 | (hg/generator (template 74 | {~not-empty-string {:title ~not-empty-string 75 | :conditions {:all (vec (& ~condition))} 76 | :actions (vec (& {"field" "priority" 77 | "value" "high"}))}})))) 78 | 79 | (defn no-shared-keys [& maps] 80 | (if (some empty? maps) 81 | true 82 | (->> maps 83 | (map keys) 84 | flatten 85 | (apply distinct?)))) 86 | 87 | (defn requirements-gen [parameters] 88 | (chuck-gen/for [:parallel [ticket-fields ticket-fields-gen 89 | targets (targets-gen parameters)] 90 | 91 | triggers (triggers-gen (keys ticket-fields)) 92 | 93 | :when ^{:max-tries 1000} (no-shared-keys ticket-fields targets triggers)] 94 | 95 | {:ticket_fields ticket-fields 96 | :targets targets 97 | :triggers triggers})) 98 | 99 | (def parameters-gen 100 | (hg/generator (template 101 | [{:type "text" 102 | :name ~not-empty-string 103 | :required bool 104 | :secure bool 105 | :default ~not-empty-string}]))) 106 | 107 | (def app-gen 108 | (chuck-gen/for [parameters parameters-gen 109 | :parallel [requirements (requirements-gen parameters) 110 | app-name (hg/generator not-empty-string)] 111 | requirements-only gen/boolean 112 | manifest (manifest-gen requirements-only parameters) 113 | :let [app-js (not requirements-only)]] 114 | {:manifest manifest 115 | :requirements requirements 116 | :templates [] 117 | :app-name app-name 118 | :app-js app-js 119 | :translations [(:default-locale manifest)] 120 | :assets []})) 121 | 122 | (defn install-gen [specific-app-gen] 123 | (chuck-gen/for [settings (hg/generator (template {:name ~not-empty-string})) 124 | app specific-app-gen 125 | enabled gen/boolean] 126 | {:app app 127 | :settings settings 128 | :enabled enabled})) 129 | 130 | (defn generate-app [] (rand-nth (sample app-gen 2))) 131 | (defn generate-installation [app] (rand-nth (sample (install-gen app) 2))) 132 | 133 | (defn requires-ticket-fields? [app] 134 | (-> app :requirements :ticket_fields empty? not)) 135 | 136 | (defn lazy-contains? [col key] 137 | (some #{key} col)) 138 | 139 | (def actions 140 | #{{:name :create-app 141 | :possibility-check (fn [state] 142 | true) 143 | :generator (fn [state] app-gen) 144 | :perform (fn [before-state expected-state app-to-create] 145 | (let [app-dir (serialize-app-to-tmpdir! app-to-create) 146 | zip-file (zip app-dir) 147 | app-name (:app-name app-to-create) 148 | app-id (upload-and-create-app zip-file app-name)] 149 | (assoc expected-state :apps 150 | (map (fn [app] 151 | (if (= app app-to-create) 152 | (assoc app :id app-id) 153 | app)) 154 | (:apps expected-state))))) 155 | :transform (fn [state app] 156 | (update state :apps conj app))} 157 | ; TODO: check response is correct 158 | {:name :delete-app 159 | :generator (fn [state] 160 | (gen/elements (:apps state))) 161 | :possibility-check (fn [state] 162 | (> (count (:apps state)) 0)) 163 | :perform (fn [before-state expected-state app-to-delete] 164 | (pprint expected-state) 165 | (let [created-app (first (filter (fn [app] 166 | (= app-to-delete 167 | (dissoc app :id))) 168 | (:apps before-state)))] 169 | (delete-app (:id created-app))) 170 | expected-state) 171 | :transform (fn [state app-to-delete] 172 | (let [app-to-delete-id (->> (:apps state) 173 | (filter (fn [app] 174 | (= (:name app) 175 | (:name app-to-delete)))) 176 | first 177 | :id)] 178 | (-> state 179 | (update-in [:apps] 180 | (partial remove #(= (:id %) app-to-delete-id))) 181 | (update-in [:installations] 182 | (partial remove #(= (:app-id %) app-to-delete-id))))))} 183 | ; gonna have some sort of assert associated with it? 184 | ; no! “types” are, like app or installation or whatever 185 | ; maybe will even split out into separate map at some stage 186 | {:name :install-app 187 | :possibility-check (fn [state] 188 | ; if there's an app without required ticket fields 189 | ; or if there's one with required ticket fields that hasn't been installed yet 190 | (let [apps (:apps state) 191 | there-are-apps (seq apps)] 192 | (and there-are-apps 193 | (let [apps-that-requires-ticket-field (filter requires-ticket-fields? 194 | apps)] 195 | (or (empty? apps-that-requires-ticket-field) 196 | (let [installed-apps (map :app (:installations state)) 197 | 198 | installed? (partial lazy-contains? installed-apps)] 199 | (not-every? installed? 200 | apps-that-requires-ticket-field))))))) 201 | :generator (fn [state] 202 | (let [apps (:apps state) 203 | appropriate-app? (fn [app] 204 | (let [installed-apps (map :app (:installations state)) 205 | not-installed? (fn [app] 206 | (not (lazy-contains? installed-apps app))) 207 | can-be-installed-twice? (comp not requires-ticket-fields?)] 208 | (or (not-installed? app) 209 | (can-be-installed-twice? app))))] 210 | (install-gen (gen/elements (filter appropriate-app? apps))))) 211 | :perform (fn [before-state expected-state installation-to-create] 212 | (let [app-to-install (first (filter (fn [app] 213 | (= (:app installation-to-create) 214 | (dissoc app :id))) 215 | (:apps expected-state))) 216 | installation-id (install-app (-> installation-to-create 217 | (dissoc :app) 218 | (assoc :app-id (:id app-to-install))))] 219 | (assoc expected-state :installations 220 | (map (fn [installation] 221 | (if (= installation installation-to-create) 222 | (assoc installation :id installation-id) 223 | installation)) 224 | (:installations expected-state))))) 225 | :transform (fn [state installation] 226 | (update-in state [:installations] conj installation))} 227 | 228 | {:name :uninstall-app 229 | :generator (fn [state] 230 | (gen/elements (:installations state))) 231 | :perform (fn [before-state expected-state installation-to-uninstall] 232 | (let [installations (:installations before-state) 233 | installation-to-uninstall-id (->> installations 234 | (filter (fn [installation] 235 | (= (:name installation) 236 | (:name installation-to-uninstall)))) 237 | first 238 | :id)] 239 | (uninstall-app installation-to-uninstall-id) 240 | expected-state)) 241 | :possibility-check (fn [state] 242 | (> (count (:installations state)) 0)) 243 | :transform (fn [state installation] 244 | (let [install-id (:id (rand-nth (:installations state)))] 245 | (update-in state [:installations] 246 | (partial remove #(= (:id %) install-id)))))}}) 247 | 248 | (defn jg [steps] 249 | (let [step-sym (fn [sym-name step-num] 250 | (symbol (str sym-name step-num))) 251 | gen-step (fn [step] 252 | (let [action (step-sym "action" step) 253 | state (step-sym "state" step) 254 | thing (step-sym "thing" step)] 255 | (template [~action (gen/such-that (fn [action] ((:possibility-check action) ~state)) 256 | (gen/elements actions) 257 | 50) 258 | ~thing ((:generator ~action) ~state) 259 | :let [~(step-sym "state" (inc step)) ((:transform ~action) ~state ~thing)]]))) 260 | step-list (range 1 (inc steps))] 261 | (template 262 | (let [state1 {}] 263 | (chuck-gen/for [~@(apply concat (map gen-step step-list))] 264 | 265 | ~(mapv (fn [step] 266 | [(step-sym "action" step) (step-sym "thing" step)]) 267 | step-list)))))) 268 | 269 | (defmacro journey-gen1 [steps] 270 | (jg steps)) 271 | 272 | (def journey-gen (journey-gen1 20)) 273 | (def journey-gen-two (journey-gen1 2)) 274 | 275 | ; TODO: if app has settings, install with values for those settings 276 | --------------------------------------------------------------------------------