├── .gitignore ├── src └── caller_id │ ├── entities.clj │ ├── response.clj │ ├── sql │ └── queries.sql │ ├── core.clj │ ├── ingest_data.clj │ ├── phone.clj │ ├── handler.clj │ └── db.clj ├── test └── caller_id │ ├── phone_test.clj │ ├── ingest_test.clj │ └── handler_test.clj ├── project.clj ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | target/ 4 | .lein* 5 | /.nrepl-port -------------------------------------------------------------------------------- /src/caller_id/entities.clj: -------------------------------------------------------------------------------- 1 | (ns caller-id.entities 2 | (:require [schema.core :as s] 3 | [caller-id.phone :as phone] 4 | [cats.monad.maybe :as maybe] 5 | [taoensso.timbre :as timbre])) 6 | 7 | ;; PhoneRecord schema 8 | (s/defschema PhoneRecord 9 | {:number s/Str 10 | :context s/Str 11 | :name s/Str}) 12 | 13 | (def p-validator (s/validator PhoneRecord)) 14 | 15 | (defn validate-phone-record [record] 16 | "Returns a validation result in a maybe monad" 17 | (try (p-validator record) 18 | (maybe/just record) 19 | (catch Exception e 20 | (timbre/error (.getMessage e)) 21 | (maybe/nothing)))) -------------------------------------------------------------------------------- /src/caller_id/response.clj: -------------------------------------------------------------------------------- 1 | (ns caller-id.response 2 | (:require [ring.util.http-response :refer :all] 3 | [cats.monad.exception :as exc] 4 | [cats.core :as m] 5 | [taoensso.timbre :as timbre] 6 | [cats.core :refer [extract fmap]] 7 | [cats.monad.either :as either])) 8 | 9 | (defn get-response 10 | "Process a Get request. Expects an either monad" 11 | [result-m] 12 | (either/branch result-m #(apply % nil) (fn [entity] 13 | (if (nil? entity) 14 | (not-found {:reason "Resource not found"}) 15 | (ok {:results entity}))))) 16 | (defn post-response 17 | "Process a POST request -- expects an either monad" 18 | ([result-m] (either/branch result-m #(apply % nil) (fn [x] (created))))) 19 | 20 | -------------------------------------------------------------------------------- /src/caller_id/sql/queries.sql: -------------------------------------------------------------------------------- 1 | -- :name create-caller-table 2 | -- :command :execute 3 | -- :result :raw 4 | -- :doc Create Caller table 5 | CREATE TABLE callers ( 6 | id INTEGER AUTO_INCREMENT PRIMARY KEY, 7 | number VARCHAR(15), 8 | context VARCHAR(64), 9 | name VARCHAR(64), 10 | ); 11 | 12 | -- :name create-phone-index 13 | -- :command :execute 14 | -- :result :raw 15 | -- :doc Create index on phone_number column 16 | CREATE UNIQUE INDEX uq_phone_ctx_idx ON callers(number, context); 17 | 18 | -- :name caller-by-row :? :1 19 | -- :doc Get Caller by ROW ID 20 | SELECT number, context, name FROM callers WHERE id = :id; 21 | 22 | -- :name callers-by-phone :? :* 23 | -- :doc Get a list of callers by phone number 24 | SELECT number, context, name FROM callers WHERE number = :number; 25 | 26 | -- :name insert-caller :! :n 27 | -- :doc Inserts a single caller 28 | INSERT INTO callers (number, context, name) VALUES (:number, :context, :name); 29 | 30 | -- :name insert-callers :! :n 31 | -- :doc Inserts multiple callers 32 | INSERT INTO callers (number, context, name) VALUES :tuple*:callers; 33 | 34 | -- :name count-callers :? :1 35 | -- :doc returns the number of callers in the table 36 | SELECT COUNT(*) FROM callers; -------------------------------------------------------------------------------- /src/caller_id/core.clj: -------------------------------------------------------------------------------- 1 | (ns caller-id.core 2 | (:gen-class) 3 | (:require [ring.adapter.jetty :refer [run-jetty]] 4 | [clojure.tools.cli :as cli] 5 | [mount.core :as mount] 6 | [caller-id.db :as db] 7 | [caller-id.ingest-data :refer [ingest-seed-data]] 8 | [caller-id.handler :refer [app]] 9 | [taoensso.timbre :as timbre])) 10 | 11 | (timbre/set-level! :info) 12 | 13 | (def cli-options 14 | ;; Set command line options 15 | [["-p" "--port PORT" "Port number" 16 | :default 8001 17 | :parse-fn #(Integer/parseInt %) 18 | :validate [#(< 0 % 0x10000) "Must be a number between 0 and 65536"]]]) 19 | 20 | (defn -main [& args] 21 | (let [cli-opts (cli/parse-opts args cli-options)] 22 | (if (:errors cli-opts) 23 | ;; If the command line options are erroneous, output the error 24 | (timbre/error (reduce #(str %1 ", " %2) (:errors cli-opts))) 25 | ;; Otherwise, start jetty 26 | (let [port (get-in cli-opts [:options :port])] 27 | (timbre/info "Initializing Database...") 28 | (mount/start) 29 | (db/init-db) 30 | (timbre/info "Ingesting seed data...") 31 | (ingest-seed-data) 32 | (timbre/info "Starting Jetty on port" port) 33 | (run-jetty app {:port port}))))) 34 | -------------------------------------------------------------------------------- /test/caller_id/phone_test.clj: -------------------------------------------------------------------------------- 1 | (ns caller-id.phone-test 2 | (:require [clojure.test :refer :all] 3 | [caller-id.phone :refer :all] 4 | [cats.monad.either :as either] 5 | [cats.monad.maybe :as maybe])) 6 | 7 | (deftest test-number-parsing 8 | (testing "Test good numbers" 9 | (let [num1 (parse-number "+41-22-767-6111") 10 | num2 (parse-number "+1-201-555-5555")] 11 | (is (either/right? num1)) 12 | (is (either/right? num2)))) 13 | (testing "Test bad numbers" 14 | (let [badnum1 (parse-number "lizard") 15 | badnum2 (parse-number "43lizards")] 16 | (is (either/left? badnum1)) 17 | (is (either/left? badnum2))))) 18 | 19 | (deftest test-number-formatting 20 | (testing "Test formatting" 21 | (let [num1 (parse-number "+41 22 767 6111") 22 | num2 (parse-number "1-201-555-5555")] 23 | (is (= (deref (format-number-m num1)) "+41227676111")) 24 | (is (= (deref (format-number-m num2)) "+12015555555"))))) 25 | 26 | (deftest test-e164-from-string 27 | (testing "Test E164 From String" 28 | (let [num1 (e164-from-string "+41 22 767 6111") 29 | num2 (e164-from-string "1-201-555-5555")] 30 | (is (= (deref num1) "+41227676111")) 31 | (is (= (deref num2) "+12015555555")))) 32 | (testing "Test Bad number" 33 | (let [badnum1 (e164-from-string "lizard")] 34 | (is (either/left? badnum1))))) -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject caller-id "0.1.0-SNAPSHOT" 2 | :description "Caller ID Task Solution" 3 | :dependencies [[org.clojure/clojure "1.8.0"] 4 | [metosin/compojure-api "1.1.10"] 5 | [ring/ring-jetty-adapter "1.6.1"] 6 | [org.clojure/tools.cli "0.3.5"] 7 | [conman "0.6.3"] 8 | [funcool/cats "2.1.0"] 9 | [mount "0.1.11"] 10 | [com.h2database/h2 "1.4.195"] 11 | [org.clojure/java.jdbc "0.6.1"] 12 | [com.layerware/hugsql "0.4.7"] 13 | [com.fzakaria/slf4j-timbre "0.3.5"] 14 | [com.googlecode.libphonenumber/libphonenumber "8.4.3"] 15 | [com.taoensso/timbre "4.10.0"] 16 | [org.clojure/data.csv "0.1.4"] 17 | [semantic-csv "0.2.0"]] 18 | :ring {:handler caller-id.handler.app} 19 | :main caller-id.core 20 | :resource-paths ["resources"] 21 | :profiles {:uberjar {:aot :all 22 | :uberjar-name "caller_id.jar"} 23 | :dev {:dependencies [[javax.servlet/javax.servlet-api "3.1.0"] 24 | [cheshire "5.6.3"] 25 | [org.clojure/tools.nrepl "0.2.13"]] 26 | :plugins [[lein-ring "0.10.0"]]} 27 | :test {:dependencies [[ring/ring-mock "0.3.0"] 28 | [cheshire "5.7.1"] 29 | [ring/ring-mock "0.3.0"] 30 | [org.clojure/tools.nrepl "0.2.13"]] 31 | :plugins [[lein-ring "0.10.0"]]}}) 32 | -------------------------------------------------------------------------------- /src/caller_id/ingest_data.clj: -------------------------------------------------------------------------------- 1 | (ns caller-id.ingest-data 2 | (:require [caller-id.db :as db] 3 | [clojure.java.io :as io] 4 | [clojure.data.csv :as csv] 5 | [semantic-csv.core :as sc :refer :all] 6 | [caller-id.entities :refer [PhoneRecord 7 | validate-phone-record]] 8 | [schema.core :as s] 9 | [cats.monad.either :as either] 10 | [taoensso.timbre :as timbre] 11 | [cats.monad.maybe :as maybe] 12 | [caller-id.phone :as phone] 13 | [ring.util.http-response :as resp])) 14 | 15 | (def seed-data-file "data") 16 | (def ingest-batch-size 50000) 17 | 18 | (defn prep-record [record] 19 | "Validates a record, logs error if any. Returns a Maybe Monad" 20 | (-> record 21 | (validate-phone-record) 22 | (phone/e164-from-record-m))) 23 | 24 | (defn ingest-record [record] 25 | "Ingest a single record. Returns an Either monad" 26 | (maybe/maybe (either/left #(resp/bad-request {:reason "Bad Input"})) 27 | (prep-record record) 28 | #(db/insert-record! %))) 29 | 30 | (defn ingest-batch [batch] 31 | "processes a batch of records" 32 | (timbre/info "ingesting...") 33 | (->> batch 34 | (map prep-record) 35 | (maybe/cat-maybes) 36 | (doall) 37 | (db/insert-records!))) 38 | 39 | (defn ingest-seed-data [] 40 | "Ingests the seed data" 41 | (with-open [data (io/reader (io/resource seed-data-file))] 42 | (let [batches (->> (csv/read-csv data) 43 | (mappify {:header ["number" "context" "name"]}) 44 | (partition-all ingest-batch-size))] 45 | (doseq [b batches] (ingest-batch b))))) 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # caller-id 2 | 3 | Caller ID task solution. Contains an embedded Jetty server. 4 | 5 | 6 | ## Usage 7 | If using leiningen: `lein run --port=` 8 | 9 | Create an uberjar with `lein uberjar` 10 | 11 | Run an uberjar with `java -jar caller_id.jar --port=` 12 | 13 | Defaults to port 8001 if none is specified. 14 | 15 | API is located at `http://localhost:/api` 16 | 17 | - Query: `GET http://localhost:/api/query?number=` 18 | ``` 19 | Results will be in the format: 20 | { 21 | "results": [ 22 | { 23 | "number": "string", 24 | "context": "string", 25 | "name": "string" 26 | }, 27 | ... 28 | ] 29 | } 30 | ``` 31 | 32 | - Number: `POST http://localhost:/api/number` 33 | * Body should be JSON with the following schema: 34 | ``` 35 | { 36 | "number":"string", 37 | "context":"string", 38 | "name":"string" 39 | } 40 | ``` 41 | 42 | A swagger interface is provided for testing; point your browser at 43 | `http://localhost:` 44 | 45 | ### Run the tests 46 | 47 | `lein test` 48 | 49 | ### Notes 50 | 51 | Uses an in-memory H2 database to store data. Ingesting the seed data may take a minute. 52 | I've batched up the inserts, hopefully that helps. 53 | 54 | The seed data appears to contain a few (I count 4) records which violate 55 | the unique constraint specified in the instructions. 56 | I'm using a UNIQUE constraint in the H2 database to enforce this. 57 | You'll see a few warnings for this in the log, these records are skipped and not loaded into the DB. 58 | 59 | I'm using the google [libphonenumber](https://github.com/googlei18n/libphonenumber) 60 | to parse, format, and validate phone numbers. 61 | -------------------------------------------------------------------------------- /test/caller_id/ingest_test.clj: -------------------------------------------------------------------------------- 1 | (ns caller-id.ingest-test 2 | (:require [clojure.test :refer :all] 3 | [caller-id.ingest-data :refer :all] 4 | [cats.monad.either :as either] 5 | [mount.core :as mount] 6 | [caller-id.db :as db] 7 | [taoensso.timbre :as timbre])) 8 | 9 | ;; Setup a test fixture 10 | ;; Need to start/top the database connection for this one 11 | (defn db-test-fixture [f] 12 | (mount/start) 13 | (db/init-db) 14 | (f) 15 | (mount/stop)) 16 | 17 | (use-fixtures :once db-test-fixture) 18 | 19 | (def test-record 20 | {:number "(719) 467-8901" :context "work" :name "Alice"}) 21 | 22 | (def test-batch 23 | [{:number "(303) 456-7890" :context "home" :name "Steve"} 24 | {:number "(719) 467-8900" :context "work" :name "Sally"} 25 | {:number "1-201-456-7890" :context "mobile" :name "Steve"}]) 26 | 27 | (deftest test-ingest 28 | (testing "Test ingest-record function" 29 | (let [result (ingest-record test-record) 30 | num (db/num-callers) 31 | fetched (db/fetch-records-by-number "+17194678901")] 32 | (is (either/right? result)) 33 | (is (= num 1)) 34 | (is (either/right? fetched)) 35 | (is (= (:name (first (deref fetched))) "Alice"))))) 36 | 37 | (deftest test-ingest-batch 38 | (testing "Test ingest-batch function" 39 | (let [result (ingest-batch test-batch) 40 | num (db/num-callers) 41 | fetch1 (db/fetch-records-by-number "+13034567890")] 42 | (is (either/right? fetch1)) 43 | (is (>= num 3)) 44 | (is (= (:name (first (deref fetch1))) "Steve"))))) 45 | 46 | (deftest test-ingest-seed-data 47 | (testing "Test ingest the seed data" 48 | (ingest-seed-data) 49 | (let [num (db/num-callers)] 50 | (is (>= num 499996)) 51 | (timbre/info "Number of rows: " num)))) -------------------------------------------------------------------------------- /src/caller_id/phone.clj: -------------------------------------------------------------------------------- 1 | (ns caller-id.phone 2 | (:require [cats.monad.either :as either] 3 | [taoensso.timbre :as timbre] 4 | [cats.core :as m] 5 | [cats.monad.maybe :as maybe]) 6 | (:import (com.google.i18n.phonenumbers PhoneNumberUtil 7 | NumberParseException 8 | PhoneNumberUtil$PhoneNumberFormat 9 | Phonenumber$PhoneNumber MetadataManager))) 10 | 11 | ;; phone-util object for usage 12 | (def phone-util (PhoneNumberUtil/getInstance)) 13 | 14 | (defn valid-number? [^Phonenumber$PhoneNumber num] 15 | "does a length check to determine validity" 16 | (.isPossibleNumber phone-util num)) 17 | 18 | 19 | (defn parse-number [^String num] 20 | "Parses a phone number string into a PhoneNumber object. 21 | Returns an either monad" 22 | (try (let [phone (.parse phone-util num "US")] 23 | (if (valid-number? phone) 24 | (either/right phone) 25 | (either/left "Not a valid phone number"))) 26 | (catch NumberParseException npe 27 | (timbre/error (str "Exception caught: " (.getMessage npe))) 28 | (either/left (.getMessage npe))))) 29 | 30 | (defn format-number ^String [^Phonenumber$PhoneNumber num] 31 | "Formats the phone number as E164" 32 | (.format phone-util num PhoneNumberUtil$PhoneNumberFormat/E164)) 33 | 34 | (def format-number-m (m/lift-m format-number)) 35 | 36 | (defn e164-from-string [^String num] 37 | "Takes in a string and returns an e164 formatted string wrapped in an either monad" 38 | (-> num 39 | (parse-number) 40 | (format-number-m))) 41 | 42 | 43 | (defn e164-from-record-m [record-m] 44 | "Takes a record wrapped in a maybe monad and formats its number string. 45 | Returning another maybe monad" 46 | (let [record (maybe/from-maybe record-m) 47 | num (e164-from-string (:number record))] 48 | (either/branch num 49 | (fn [e] 50 | (timbre/error "Error on number: " (:number record) e) 51 | (maybe/nothing)) 52 | #(maybe/just (assoc record :number %))))) 53 | -------------------------------------------------------------------------------- /src/caller_id/handler.clj: -------------------------------------------------------------------------------- 1 | (ns caller-id.handler 2 | (:require [compojure.api.sweet :refer :all] 3 | [ring.util.http-response :refer [bad-request! 4 | internal-server-error!]] 5 | [caller-id.response :as resp] 6 | [schema.core :as s] 7 | [caller-id.entities :refer [PhoneRecord]] 8 | [caller-id.ingest-data :refer [ingest-record]] 9 | [caller-id.phone :as phone] 10 | [caller-id.db :refer [fetch-records-by-number]] 11 | [caller-id.db :as db] 12 | [cats.monad.either :as either] 13 | [taoensso.timbre :as timbre])) 14 | 15 | (defn wrap-request-logging 16 | "Logs incoming request" 17 | [handler] 18 | (fn [req] 19 | (timbre/info "REQ FROM " 20 | (:remote-addr req) 21 | (:request-method req) 22 | (:uri req) 23 | (:query-string req)) 24 | (handler req))) 25 | 26 | (def app 27 | (api 28 | {:swagger 29 | {:ui "/" 30 | :spec "/swagger.json" 31 | :data {:info {:title "Caller-Id" 32 | :description "Caller ID API"} 33 | :tags [{:name "api", :description "API for Caller ID Data"}]}}} 34 | 35 | (context "/api" [] 36 | :tags ["api"] 37 | 38 | ;; This returns a collection of records matching the numbers 39 | ;; Debated whether to return an empty collection w/ a 200 or a 404 in the case of nothing found 40 | ;; Decided to go with the empty collection 41 | ;; I find its easier/cleaner to deal with an empty list rather than an error code on the front end, anyway. 42 | (GET "/query" [] 43 | :return {:results [PhoneRecord]} 44 | :middleware [wrap-request-logging] 45 | :query-params [number :- String] 46 | :summary "Given a phone number, returns a list of matching records" 47 | (let [phone-number (phone/e164-from-string number)] 48 | (either/branch (phone/e164-from-string number) 49 | #(bad-request! {:reason %}) 50 | #(resp/get-response (fetch-records-by-number %))))) 51 | 52 | (POST "/number" [] 53 | :middleware [wrap-request-logging] 54 | :body [pr PhoneRecord] 55 | :summary "Adds a record to the database" 56 | (resp/post-response (ingest-record pr)))))) 57 | -------------------------------------------------------------------------------- /src/caller_id/db.clj: -------------------------------------------------------------------------------- 1 | (ns caller-id.db 2 | (:require [conman.core :as conman] 3 | [hugsql.core :as hugsql] 4 | [mount.core :refer [defstate]] 5 | [cats.monad.either :as either] 6 | [ring.util.http-response :as resp] 7 | [taoensso.timbre :as timbre] 8 | [clojure.java.jdbc :as jdbc] 9 | [cats.core :as m]) 10 | (:import (java.sql SQLException))) 11 | 12 | ;; Our database settings 13 | (def db-settings {:jdbc-url "jdbc:h2:mem:callerid"}) 14 | 15 | (defn connect! [] 16 | "Establishes a Database connection" 17 | (conman/connect! db-settings)) 18 | 19 | (defn disconnect! [conn] 20 | "Destroys database connection" 21 | (conman/disconnect! conn)) 22 | 23 | (defstate ^:dynamic *db* 24 | :start (connect!) 25 | :stop (disconnect! *db*)) 26 | 27 | ;; This exposes the SQL queries in 'queries.sql' as Clojure functions 28 | (conman/bind-connection *db* "caller_id/sql/queries.sql") 29 | 30 | (defn init-db [] 31 | "Creates database tables" 32 | ;; Creates the Caller SQL Table in H2 33 | (create-caller-table) 34 | ;; Creates a Unique index on (number, context) 35 | (create-phone-index)) 36 | 37 | (defn handle-sql-error 38 | "Logs and sets an aprropriate response" 39 | [se] 40 | (let [sqlstate (.getSQLState se)] 41 | (timbre/warn (str "SQL Error occured with state: " sqlstate "\n") se) 42 | (cond 43 | (= "23505" sqlstate) #(resp/conflict {:reason "Entity conflicts with existing resource"}) 44 | (= "23514" sqlstate) #(resp/conflict {:reason "Conflict: A newer version of this data exists on the server"}) 45 | :else #(resp/internal-server-error {:reason "Database error"})))) 46 | 47 | (defn handle-other-error 48 | "Something bad, but not a sql exception, happened while accessing the database" 49 | [ex] 50 | (timbre/error (str "Error occurred! " (.getName (.getClass ex)) " " (.getMessage ex) "\n") ex) 51 | #(resp/internal-server-error {:reason "Error while accessing database"})) 52 | 53 | (defn wrap-db 54 | "Wraps a database query. Returns an Either monad" 55 | [query] 56 | (try (either/right (query)) 57 | (catch SQLException se (either/left (handle-sql-error se))) 58 | (catch Exception ex (either/left (handle-other-error ex))))) 59 | 60 | (defn insert-record! [record] 61 | "Inserts a single record into the database" 62 | (wrap-db #(insert-caller record))) 63 | 64 | (defn insert-records! [records] 65 | "Inserts records into the database" 66 | (conman/with-transaction 67 | [*db*] 68 | (doseq [r records] (wrap-db #(insert-caller r))))) 69 | 70 | (defn fetch-records-by-number [^String number] 71 | "Fetches an individual record given a phone number" 72 | (wrap-db #(callers-by-phone {:number number}))) 73 | 74 | (defn fetch-record-by-id [^Integer id] 75 | "Fetches an individual record given its row id" 76 | (wrap-db #(caller-by-row {:id id}))) 77 | 78 | (defn num-callers [] 79 | "counts the number of callers in the database table" 80 | (let [c (count-callers)] 81 | (val (first c)))) 82 | -------------------------------------------------------------------------------- /test/caller_id/handler_test.clj: -------------------------------------------------------------------------------- 1 | (ns caller-id.handler-test 2 | (:require [cheshire.core :as cheshire] 3 | [clojure.test :refer :all] 4 | [caller-id.handler :refer :all] 5 | [caller-id.ingest-data :refer [ingest-batch]] 6 | [ring.mock.request :as mock] 7 | [caller-id.db :as db] 8 | [mount.core :as mount])) 9 | 10 | (defn parse-body [body] 11 | (cheshire/parse-string (slurp body) true)) 12 | 13 | (def test-record 14 | {:number "(719) 467-8901" :context "work" :name "Alice"}) 15 | 16 | (def test-seed 17 | [{:number "(303) 456-7890" :context "home" :name "Steve"} 18 | {:number "(719) 467-8900" :context "work" :name "Sally"} 19 | {:number "1-201-456-7890" :context "mobile" :name "Steve"} 20 | {:number "(303) 456-7890" :context "vacation" :name "Steve"}]) 21 | 22 | (defn setup [] 23 | "Setup test env" 24 | (mount/start) 25 | (db/init-db) 26 | (ingest-batch test-seed)) 27 | 28 | (defn cleanup [] 29 | "Tears down test env" 30 | (mount/stop)) 31 | 32 | (defn api-test-fixture [f] 33 | (setup) 34 | (f) 35 | (cleanup)) 36 | 37 | (use-fixtures :once api-test-fixture) 38 | 39 | (defn mock-post [record] 40 | (-> (mock/request :post "/api/number" (cheshire/encode record)) 41 | (mock/content-type "application/json"))) 42 | 43 | (deftest api-test 44 | (testing "Test GET request that should return a single record" 45 | (let [response (app (-> (mock/request :get "/api/query?number=+17194678900"))) 46 | body (parse-body (:body response)) 47 | record (first (:results body))] 48 | (is (= (:status response) 200)) 49 | (is (= (:name record) "Sally")))) 50 | (testing "Test GET request that should return two records" 51 | (let [response (app (-> (mock/request :get "/api/query?number=+13034567890"))) 52 | body (parse-body (:body response)) 53 | results (:results body)] 54 | (is (= (:status response) 200)) 55 | (is (= (count results) 2)) 56 | (is (= (:name (first results)) (:name (second results)) "Steve")))) 57 | (testing "Test GET request that should return no results" 58 | (let [response (app (-> (mock/request :get "/api/query?number=+18001234567"))) 59 | body (parse-body (:body response)) 60 | results (:results body)] 61 | (is (= (:status response) 200)) 62 | (is (= (count results) 0)))) 63 | (testing "Test GET request with an invalid number" 64 | (let [response (app (-> (mock/request :get "/api/query?number=42")))] 65 | ;; Should return 400 66 | (is (= (:status response) 400)))) 67 | (testing "Test GET request with a bad query parameter" 68 | (let [response (app (-> (mock/request :get "/api/query?lizard=iguana")))] 69 | ;; Should return 400 70 | (is (= (:status response) 400)))) 71 | (testing "Test POST request" 72 | (let [response (app (-> (mock-post test-record))) 73 | fetch-resp (deref (db/fetch-records-by-number "+17194678901"))] 74 | ;; Should return 201 75 | (is (= (:status response) 201)) 76 | ;; Verify it was in fact ingested 77 | (is (= (:name (first fetch-resp)) "Alice")))) 78 | (testing "Test POST request with invalid record" 79 | (let [response (app (-> (mock-post {:lizard "iguana"})))] 80 | ;; Should return 400 81 | (is (= (:status response) 400)))) 82 | (testing "Test POST request with invalid number" 83 | (let [response (app (-> (mock-post {:number "42" :context "beach" :name "Toni"})))] 84 | ;; Should return 400 85 | (is (= (:status response) 400)))) 86 | (testing "Test POST with no body" 87 | (let [response (app (-> (mock-post nil)))] 88 | ;; Should return 400 89 | (is (= (:status response) 400)))) 90 | (testing "Test POST an already existing record" 91 | (let [response (app (-> (mock-post test-record)))] 92 | ;; Should return 409 - conflict 93 | (is (= (:status response) 409))))) 94 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | caller-id 4 | caller-id 5 | jar 6 | 0.1.0-SNAPSHOT 7 | caller-id 8 | Caller ID Task Solution 9 | 10 | scm:git:git://github.com/rhinoman/caller-id.git 11 | scm:git:ssh://git@github.com/rhinoman/caller-id.git 12 | 8b9f5ebd068e0c759ccdbc2416cebd075af636b5 13 | 14 | https://github.com/rhinoman/caller-id 15 | 16 | 17 | src 18 | test 19 | 20 | 21 | resources 22 | 23 | 24 | 25 | 26 | resources 27 | 28 | 29 | target 30 | target/classes 31 | 32 | 33 | 34 | 35 | central 36 | https://repo1.maven.org/maven2/ 37 | 38 | false 39 | 40 | 41 | true 42 | 43 | 44 | 45 | clojars 46 | https://clojars.org/repo/ 47 | 48 | true 49 | 50 | 51 | true 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | org.clojure 61 | clojure 62 | 1.8.0 63 | 64 | 65 | metosin 66 | compojure-api 67 | 1.1.10 68 | 69 | 70 | ring 71 | ring-jetty-adapter 72 | 1.6.1 73 | 74 | 75 | org.clojure 76 | tools.cli 77 | 0.3.5 78 | 79 | 80 | conman 81 | conman 82 | 0.6.3 83 | 84 | 85 | funcool 86 | cats 87 | 2.1.0 88 | 89 | 90 | mount 91 | mount 92 | 0.1.11 93 | 94 | 95 | com.h2database 96 | h2 97 | 1.4.195 98 | 99 | 100 | org.clojure 101 | java.jdbc 102 | 0.6.1 103 | 104 | 105 | com.layerware 106 | hugsql 107 | 0.4.7 108 | 109 | 110 | com.fzakaria 111 | slf4j-timbre 112 | 0.3.5 113 | 114 | 115 | com.googlecode.libphonenumber 116 | libphonenumber 117 | 8.4.3 118 | 119 | 120 | com.taoensso 121 | timbre 122 | 4.10.0 123 | 124 | 125 | org.clojure 126 | data.csv 127 | 0.1.4 128 | 129 | 130 | semantic-csv 131 | semantic-csv 132 | 0.2.0 133 | 134 | 135 | javax.servlet 136 | javax.servlet-api 137 | 3.1.0 138 | test 139 | 140 | 141 | cheshire 142 | cheshire 143 | 5.7.1 144 | test 145 | 146 | 147 | org.clojure 148 | tools.nrepl 149 | 0.2.13 150 | test 151 | 152 | 153 | ring 154 | ring-mock 155 | 0.3.0 156 | test 157 | 158 | 159 | 160 | 161 | 165 | --------------------------------------------------------------------------------