├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── Vagrantfile ├── puppet └── manifests │ └── default.pp └── restful-clojure ├── .gitignore ├── doc └── intro.md ├── migrations ├── 2014-03-03-213517-add-initial-tables.down.sql ├── 2014-03-03-213517-add-initial-tables.up.sql ├── 2015-03-13-005900-add-password-digest-to-users.down.sql ├── 2015-03-13-005900-add-password-digest-to-users.up.sql ├── 2015-03-13-015510-add-token-table.down.sql ├── 2015-03-13-015510-add-token-table.up.sql ├── 2015-03-13-090909-add-level-to-user.down.sql └── 2015-03-13-090909-add-level-to-user.up.sql ├── project.clj ├── src └── restful_clojure │ ├── auth.clj │ ├── db.clj │ ├── entities.clj │ ├── handler.clj │ └── models │ ├── lists.clj │ ├── products.clj │ └── users.clj └── test └── restful_clojure ├── auth_test.clj ├── handler_test.clj ├── lists_test.clj ├── products_test.clj ├── test_core.clj ├── users+lists_test.clj └── users_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /.vagrant 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "puppet/modules/stdlib"] 2 | path = puppet/modules/stdlib 3 | url = https://github.com/puppetlabs/puppetlabs-stdlib.git 4 | [submodule "puppet/modules/postgresql"] 5 | path = puppet/modules/postgresql 6 | url = https://github.com/puppetlabs/puppetlabs-postgresql.git 7 | [submodule "puppet/modules/concat"] 8 | path = puppet/modules/concat 9 | url = https://github.com/puppetlabs/puppetlabs-concat.git 10 | [submodule "puppet/modules/java7"] 11 | path = puppet/modules/java7 12 | url = https://github.com/softek/puppet-java7 13 | [submodule "puppet/modules/apt"] 14 | path = puppet/modules/apt 15 | url = https://github.com/puppetlabs/puppetlabs-apt.git 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Andrew Meredith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | restful-clojure 2 | =============== 3 | 4 | An example RESTful shopping list application back-end written in Clojure to accompany a tutorial series on kendru.github.io 5 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 5 | VAGRANTFILE_API_VERSION = "2" 6 | 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | # We're using an Ubuntu box from Puppet Labs with Puppet already installed 9 | config.vm.box = "ubuntu-puppetlabs" 10 | 11 | # The url from where the 'config.vm.box' box will be fetched if it 12 | # doesn't already exist on the user's system. 13 | config.vm.box_url = "http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210.box" 14 | 15 | # Create a private network so that we can access our VM like any other maching 16 | # on our network. We could use port forwarding instead, but we're opting to 17 | # access the VM as a separate machine to mimick a more production-like setup. 18 | config.vm.network :private_network, ip: "192.168.33.10" 19 | 20 | # Create bridged network so that our VM can access the internet through the 21 | # host machine's network. 22 | config.vm.network :public_network 23 | 24 | config.vm.synced_folder "./restful-clojure", "/vagrant" 25 | 26 | config.vm.provision "puppet" do |puppet| 27 | puppet.manifests_path = "puppet/manifests" 28 | puppet.manifest_file = "default.pp" 29 | puppet.module_path = "puppet/modules" 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /puppet/manifests/default.pp: -------------------------------------------------------------------------------- 1 | exec { 'update_packages': 2 | command => 'apt-get update', 3 | path => '/usr/bin', 4 | } 5 | 6 | # Install Leiningen to run tests and build project 7 | exec { 'get_leiningen': 8 | command => '/usr/bin/wget https://raw.github.com/technomancy/leiningen/stable/bin/lein -O /usr/bin/lein && /bin/chmod a+x /usr/bin/lein', 9 | unless => '/usr/bin/which lein &>/dev/null', 10 | } 11 | 12 | class { 'postgresql::server': } 13 | 14 | postgresql::server::db { 'restful_dev': 15 | user => 'restful_dev', 16 | password => postgresql_password('restful_dev', 'pass_dev'), 17 | } 18 | 19 | postgresql::server::db { 'restful_test': 20 | user => 'restful_test', 21 | password => postgresql_password('restful_test', 'pass_test'), 22 | } 23 | 24 | file { "/etc/profile.d/env.sh": 25 | content => "export RESTFUL_DB_URL=\"jdbc:postgresql://localhost:5432/restful_dev?user=restful_dev&password=pass_dev\"" 26 | } 27 | 28 | include java7 29 | 30 | -------------------------------------------------------------------------------- /restful-clojure/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | -------------------------------------------------------------------------------- /restful-clojure/doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to restful-clojure 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/great-documentation/what-to-write/) 4 | -------------------------------------------------------------------------------- /restful-clojure/migrations/2014-03-03-213517-add-initial-tables.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE lists_products; 2 | DROP TYPE item_status; 3 | DROP TABLE products; 4 | DROP TABLE lists; 5 | DROP TABLE users; 6 | DROP FUNCTION update_updated_at_column(); -------------------------------------------------------------------------------- /restful-clojure/migrations/2014-03-03-213517-add-initial-tables.up.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION update_updated_at_column() 2 | RETURNS TRIGGER AS ' 3 | BEGIN 4 | NEW.updated_at = NOW(); 5 | RETURN NEW; 6 | END; 7 | ' LANGUAGE 'plpgsql'; 8 | 9 | CREATE TABLE users ( 10 | id serial PRIMARY KEY, 11 | name varchar(40) NOT NULL CHECK (name <> ''), 12 | email varchar(60) NOT NULL UNIQUE, 13 | created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP 15 | ); 16 | CREATE TRIGGER update_updated_at_users 17 | BEFORE UPDATE ON users FOR EACH ROW EXECUTE 18 | PROCEDURE update_updated_at_column(); 19 | 20 | CREATE TABLE lists ( 21 | id serial PRIMARY KEY, 22 | user_id integer REFERENCES users(id) ON DELETE CASCADE, 23 | title varchar(40) NOT NULL, 24 | created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 25 | updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP 26 | ); 27 | CREATE TRIGGER update_updated_at_lists 28 | BEFORE UPDATE ON users FOR EACH ROW EXECUTE 29 | PROCEDURE update_updated_at_column(); 30 | 31 | CREATE TABLE products ( 32 | id serial PRIMARY KEY, 33 | title varchar(40) NOT NULL, 34 | description text NULL, 35 | created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP 36 | ); 37 | 38 | CREATE TYPE item_status AS ENUM ('incomplete', 'complete', 'deleted'); 39 | CREATE TABLE lists_products ( 40 | list_id integer REFERENCES lists(id) ON DELETE CASCADE, 41 | product_id integer REFERENCES products(id) ON DELETE CASCADE, 42 | status item_status NOT NULL DEFAULT 'incomplete', 43 | PRIMARY KEY (list_id, product_id) 44 | ); 45 | -------------------------------------------------------------------------------- /restful-clojure/migrations/2015-03-13-005900-add-password-digest-to-users.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP COLUMN password_digest; -------------------------------------------------------------------------------- /restful-clojure/migrations/2015-03-13-005900-add-password-digest-to-users.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN password_digest VARCHAR(162); 2 | UPDATE users SET password_digest = 'invalid'; 3 | ALTER TABLE users ALTER COLUMN password_digest SET NOT NULL; -------------------------------------------------------------------------------- /restful-clojure/migrations/2015-03-13-015510-add-token-table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE auth_tokens; -------------------------------------------------------------------------------- /restful-clojure/migrations/2015-03-13-015510-add-token-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE auth_tokens ( 2 | id VARCHAR(44) PRIMARY KEY, 3 | user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, 4 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 5 | ); 6 | CREATE INDEX ON auth_tokens (id, created_at DESC); -------------------------------------------------------------------------------- /restful-clojure/migrations/2015-03-13-090909-add-level-to-user.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP COLUMN level; -------------------------------------------------------------------------------- /restful-clojure/migrations/2015-03-13-090909-add-level-to-user.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN level VARCHAR(12) NOT NULL default 'user'; -------------------------------------------------------------------------------- /restful-clojure/project.clj: -------------------------------------------------------------------------------- 1 | (defproject restful-clojure "0.1.0-SNAPSHOT" 2 | :description "An example RESTful shopping list application back-end written in Clojure to accompany a tutorial series on kendru.github.io" 3 | :url "https://github.com/kendru/restful-clojure" 4 | :license {:name "MIT" 5 | :url "http://opensource.org/licenses/MIT"} 6 | :dependencies [[org.clojure/clojure "1.5.1"] 7 | [ring/ring-core "1.2.1"] 8 | [ring/ring-jetty-adapter "1.2.1"] 9 | [compojure "1.1.6"] 10 | [cheshire "5.3.1"] 11 | [ring/ring-json "0.2.0"] 12 | [korma "0.3.0-RC5"] 13 | [org.postgresql/postgresql "9.2-1002-jdbc4"] 14 | [ragtime "0.3.4"] 15 | [environ "0.4.0"] 16 | [buddy/buddy-hashers "0.4.0"] 17 | [buddy/buddy-auth "0.4.0"] 18 | [crypto-random "1.2.0"]] 19 | 20 | ; The lein-ring plugin allows us to easily start a development web server 21 | ; with "lein ring server". It also allows us to package up our application 22 | ; as a standalone .jar or as a .war for deployment to a servlet contianer 23 | ; (I know... SO 2005). 24 | :plugins [[lein-ring "0.8.10"] 25 | [ragtime/ragtime.lein "0.3.6"] 26 | [lein-environ "0.4.0"]] 27 | 28 | ; See https://github.com/weavejester/lein-ring#web-server-options for the 29 | ; various options available for the lein-ring plugin 30 | :ring {:handler restful-clojure.handler/app 31 | :nrepl {:start? true 32 | :port 9998}} 33 | 34 | ; Have ragtime default to loading the database URL from an environment 35 | ; variable so that we don't keep production credentials in our 36 | ; source code. Note that for our dev environment, we set this variable 37 | ; with Puppet (see default.pp). 38 | :ragtime {:migrations ragtime.sql.files/migrations 39 | :database ~(System/getenv "RESTFUL_DB_URL")} 40 | 41 | :profiles 42 | {:dev {:dependencies [[javax.servlet/servlet-api "2.5"] 43 | [ring-mock "0.1.5"]] 44 | ; Since we are using environ, we can override these values with 45 | ; environment variables in production. 46 | :env {:restful-db "restful_dev" 47 | :restful-db-user "restful_dev" 48 | :restful-db-pass "pass_dev"}} 49 | :test {:ragtime {:database "jdbc:postgresql://localhost:5432/restful_test?user=restful_test&password=pass_test"} 50 | :env {:restful-db "restful_test" 51 | :restful-db-user "restful_test" 52 | :restful-db-pass "pass_test"}}}) 53 | -------------------------------------------------------------------------------- /restful-clojure/src/restful_clojure/auth.clj: -------------------------------------------------------------------------------- 1 | (ns restful-clojure.auth 2 | (:use korma.core) 3 | (:require [restful-clojure.entities :as e] 4 | [restful-clojure.models.users :as users] 5 | [buddy.auth.backends.token :refer [token-backend]] 6 | [buddy.auth.accessrules :refer [success error]] 7 | [buddy.auth :refer [authenticated?]] 8 | [crypto.random :refer [base64]])) 9 | 10 | (defn gen-session-id [] (base64 32)) 11 | 12 | (defn make-token! 13 | "Creates an auth token in the database for the given user and puts it in the database" 14 | [user-id] 15 | (let [token (gen-session-id)] 16 | (insert e/auth-tokens 17 | (values {:id token 18 | :user_id user-id})) 19 | token)) 20 | 21 | (defn authenticate-token 22 | "Validates a token, returning the id of the associated user when valid and nil otherwise" 23 | [req token] 24 | (let [sql (str "SELECT user_id " 25 | "FROM auth_tokens " 26 | "WHERE id = ? " 27 | "AND created_at > current_timestamp - interval '6 hours'")] 28 | (some-> (exec-raw [sql [token]] :results) 29 | first 30 | :user_id 31 | users/find-by-id))) 32 | 33 | (defn unauthorized-handler [req msg] 34 | {:status 401 35 | :body {:status :error 36 | :message (or msg "User not authorized")}}) 37 | 38 | ;; Looks for an "Authorization" header with a value of "Token XXX" 39 | ;; where "XXX" is some valid token. 40 | (def auth-backend (token-backend {:authfn authenticate-token 41 | :unauthorized-handler unauthorized-handler})) 42 | 43 | ;; Map of actions to the set of user types authorized to perform that action 44 | (def permissions 45 | {"manage-lists" #{:restful-clojure.models.users/user} 46 | "manage-products" #{:restful-clojure.models.users/admin} 47 | "manage-users" #{:restful-clojure.models.users/admin}}) 48 | 49 | ;;; Below are the handlers that Buddy will use for various authorization 50 | ;;; requirements the authenticated-user function determines whether a session 51 | ;;; token has been resolved to a valid user session, and the other functions 52 | ;;; take some argument and _return_ a handler that determines whether the 53 | ;;; user is authorized for some particular scenario. See handler.clj for usage. 54 | 55 | (defn authenticated-user [req] 56 | (if (authenticated? req) 57 | true 58 | (error "User must be authenticated"))) 59 | 60 | ;; Assumes that a check for authorization has already been performed 61 | (defn user-can 62 | "Given a particular action that the authenticated user desires to perform, 63 | return a handler determining if their user level is authorized to perform 64 | that action." 65 | [action] 66 | (fn [req] 67 | (let [user-level (get-in req [:identity :level]) 68 | required-levels (get permissions action #{})] 69 | (if (some #(isa? user-level %) required-levels) 70 | (success) 71 | (error (str "User of level " (name user-level) " is not authorized for action " (name action))))))) 72 | 73 | (defn user-isa 74 | "Return a handler that determines whenther the authenticated user is of a 75 | specific level OR any derived level." 76 | [level] 77 | (fn [req] 78 | (if (isa? (get-in req [:identity :level]) level) 79 | (success) 80 | (error (str "User is not a(n) " (name level)))))) 81 | 82 | (defn user-has-id 83 | "Return a handler that determines whether the authenticated user has a given ID. 84 | This is useful, for example, to determine if the user is the owner of the requested 85 | resource." 86 | [id] 87 | (fn [req] 88 | (if (= id (get-in req [:identity :id])) 89 | (success) 90 | (error (str "User does not have id given"))))) 91 | -------------------------------------------------------------------------------- /restful-clojure/src/restful_clojure/db.clj: -------------------------------------------------------------------------------- 1 | (ns restful-clojure.db 2 | (:use korma.db) 3 | (:require [environ.core :refer [env]])) 4 | 5 | (defdb db (postgres {:db (get env :restful-db "restful_clojure") 6 | :user (get env :restful-db-user "restful_clojure") 7 | :password (get env :restful-db-pass "") 8 | :host (get env :restful-db-host "localhost") 9 | :port (get env :restful-db-port 5432)})) 10 | -------------------------------------------------------------------------------- /restful-clojure/src/restful_clojure/entities.clj: -------------------------------------------------------------------------------- 1 | (ns restful-clojure.entities 2 | (:use korma.core 3 | restful-clojure.db)) 4 | 5 | (declare users lists products) 6 | 7 | (defentity users 8 | (pk :id) 9 | (table :users) 10 | (has-many lists) 11 | (entity-fields :name :email)) 12 | 13 | (defentity lists 14 | (pk :id) 15 | (table :lists) 16 | (belongs-to users {:fk :user_id}) 17 | (many-to-many products :lists_products {:lfk :list_id 18 | :rfk :product_id}) 19 | (entity-fields :title)) 20 | 21 | (defentity products 22 | (pk :id) 23 | (table :products) 24 | (many-to-many lists :lists_products {:lfk :product_id 25 | :rfk :list_id}) 26 | (entity-fields :title :description)) 27 | 28 | (defentity auth-tokens 29 | (pk :id) 30 | (table :auth_tokens) 31 | (belongs-to users {:fk :user_id})) 32 | 33 | -------------------------------------------------------------------------------- /restful-clojure/src/restful_clojure/handler.clj: -------------------------------------------------------------------------------- 1 | (ns restful-clojure.handler 2 | (:use compojure.core 3 | ring.middleware.json) 4 | (:import (com.fasterxml.jackson.core JsonGenerator)) 5 | (:require [compojure.handler :as handler] 6 | [compojure.route :as route] 7 | [ring.util.response :refer [response]] 8 | [cheshire.generate :refer [add-encoder]] 9 | [restful-clojure.models.users :as users] 10 | [restful-clojure.models.lists :as lists] 11 | [restful-clojure.models.products :as products] 12 | [restful-clojure.auth :refer [auth-backend user-can user-isa user-has-id authenticated-user unauthorized-handler make-token!]] 13 | [buddy.auth.middleware :refer [wrap-authentication wrap-authorization]] 14 | [buddy.auth.accessrules :refer [restrict]])) 15 | 16 | ; Strip namespace from namespaced-qualified kewywords, which is how wo represent user levels 17 | (add-encoder clojure.lang.Keyword 18 | (fn [^clojure.lang.Keyword kw ^JsonGenerator gen] 19 | (.writeString gen (name kw)))) 20 | 21 | (defn get-users [_] 22 | {:status 200 23 | :body {:count (users/count-users) 24 | :results (users/find-all)}}) 25 | 26 | (defn create-user [{user :body}] 27 | (let [new-user (users/create user)] 28 | {:status 201 29 | :headers {"Location" (str "/users/" (:id new-user))}})) 30 | 31 | (defn find-user [{{:keys [id]} :params}] 32 | (response (users/find-by-id (read-string id)))) 33 | 34 | (defn lists-for-user [{{:keys [id]} :params}] 35 | (response 36 | (map #(dissoc % :user_id) (lists/find-all-by :user_id (read-string id))))) 37 | 38 | (defn delete-user [{{:keys [id]} :params}] 39 | (users/delete-user {:id (read-string id)}) 40 | {:status 204 41 | :headers {"Location" "/users"}}) 42 | 43 | (defn get-lists [_] 44 | {:status 200 45 | :body {:count (lists/count-lists) 46 | :results (lists/find-all)}}) 47 | 48 | (defn create-list [{listdata :body}] 49 | (let [new-list (lists/create listdata)] 50 | {:status 201 51 | :headers {"Location" (str "/users/" (:user_id new-list) "/lists")}})) 52 | 53 | (defn find-list [{{:keys [id]} :params}] 54 | (response (lists/find-by-id (read-string id)))) 55 | 56 | (defn update-list [{{:keys [id]} :params 57 | listdata :body}] 58 | (if (nil? id) 59 | {:status 404 60 | :headers {"Location" "/lists"}} 61 | 62 | ((lists/update-list (assoc listdata :id id)) 63 | {:status 200 64 | :headers {"Location" (str "/lists/" id)}}))) 65 | 66 | (defn delete-list [{{:keys [id]} :params}] 67 | (lists/delete-list {:id (read-string id)}) 68 | {:status 204 69 | :headers {"Location" "/lists"}}) 70 | 71 | (defn get-products [_] 72 | {:status 200 73 | :body {:count (products/count-products) 74 | :results (products/find-all)}}) 75 | 76 | (defn create-product [{product :body}] 77 | (let [new-prod (products/create product)] 78 | {:status 201 79 | :headers {"Location" (str "/products/" (:id new-prod))}})) 80 | 81 | (defroutes app-routes 82 | ;; USERS 83 | (context "/users" [] 84 | (GET "/" [] (-> get-users 85 | (restrict {:handler {:and [authenticated-user (user-can "manage-users")]} 86 | :on-error unauthorized-handler}))) 87 | (POST "/" [] create-user) 88 | (context "/:id" [id] 89 | (restrict 90 | (routes 91 | (GET "/" [] find-user) 92 | (GET "/lists" [] lists-for-user)) 93 | {:handler {:and [authenticated-user 94 | {:or [(user-can "manage-users") 95 | (user-has-id (read-string id))]}]} 96 | :on-error unauthorized-handler})) 97 | (DELETE "/:id" [id] (-> delete-user 98 | (restrict {:handler {:and [authenticated-user (user-can "manage-users")]} 99 | :on-error unauthorized-handler})))) 100 | 101 | (POST "/sessions" {{:keys [user-id password]} :body} 102 | (if (users/password-matches? user-id password) 103 | {:status 201 104 | :body {:auth-token (make-token! user-id)}} 105 | {:status 409 106 | :body {:status "error" 107 | :message "invalid username or password"}})) 108 | 109 | ;; LISTS 110 | (context "/lists" [] 111 | (GET "/" [] (-> get-lists 112 | (restrict {:handler {:and [authenticated-user (user-isa :restful-clojure.models.users/admin)]} 113 | :on-error unauthorized-handler}))) 114 | (POST "/" [] (-> create-list 115 | (restrict {:handler {:and [authenticated-user (user-can "manage-lists")]} 116 | :on-error unauthorized-handler}))) 117 | 118 | (context "/:id" [id] 119 | (let [owner-id (get (lists/find-by-id (read-string id)) :user_id)] 120 | (restrict 121 | (routes 122 | (GET "/" [] find-list) 123 | (PUT "/" [] update-list) 124 | (DELETE "/" [] delete-list)) 125 | {:handler {:and [authenticated-user 126 | {:or [(user-can "manage-lists") 127 | (user-has-id owner-id)]}]} 128 | :on-error unauthorized-handler})))) 129 | 130 | ;; PRODUCTS 131 | (context "/products" [] 132 | (restrict 133 | (routes 134 | (GET "/" [] get-products) 135 | (POST "/" [] create-product)) 136 | {:handler {:and [authenticated-user (user-can "manage-products")]} 137 | :on-error unauthorized-handler})) 138 | 139 | (route/not-found (response {:message "Page not found"}))) 140 | 141 | (defn wrap-log-request [handler] 142 | (fn [req] 143 | (println req) 144 | (handler req))) 145 | 146 | (def app 147 | (-> app-routes 148 | (wrap-authentication auth-backend) 149 | (wrap-authorization auth-backend) 150 | wrap-json-response 151 | (wrap-json-body {:keywords? true}))) 152 | -------------------------------------------------------------------------------- /restful-clojure/src/restful_clojure/models/lists.clj: -------------------------------------------------------------------------------- 1 | (ns restful-clojure.models.lists 2 | (:use korma.core) 3 | (:require [restful-clojure.entities :as e] 4 | [clojure.set :refer [difference]])) 5 | 6 | (declare add-product) 7 | 8 | (defn find-all [] 9 | (select e/lists 10 | (with e/products))) 11 | 12 | (defn find-by [field value] 13 | (first 14 | (select e/lists 15 | (with e/products) 16 | (where {field value}) 17 | (limit 1)))) 18 | 19 | (defn find-all-by [field value] 20 | (select e/lists 21 | (with e/products) 22 | (where {field value}))) 23 | 24 | (defn find-by-id [id] 25 | (find-by :id id)) 26 | 27 | (defn for-user [userdata] 28 | (find-all-by :user_id (:id userdata))) 29 | 30 | (defn count-lists [] 31 | (let [agg (select e/lists 32 | (aggregate (count :*) :cnt))] 33 | (get-in agg [0 :cnt] 0))) 34 | 35 | (defn create [listdata] 36 | (let [newlist (insert e/lists 37 | (values (dissoc listdata :products)))] 38 | (doseq [product (:products listdata)] 39 | (add-product newlist (:id product) "incomplete")) 40 | (assoc newlist :products (into [] (:products listdata))))) 41 | 42 | (defn add-product 43 | "Add a product to a list with an optional status arg" 44 | ([listdata product-id] 45 | (add-product listdata product-id "incomplete")) 46 | ([listdata product-id status] 47 | (let [sql (str "INSERT INTO lists_products (" 48 | "list_id, product_id, status" 49 | ") VALUES (" 50 | "?, ?, ?::item_status" 51 | ")")] 52 | (exec-raw [sql [(:id listdata) product-id status] :results]) 53 | (find-by-id (:id listdata))))) 54 | 55 | (defn remove-product [listdata product-id] 56 | (delete "lists_products" 57 | (where {:list_id (:id listdata) 58 | :product_id product-id})) 59 | (update-in listdata [:products] 60 | (fn [products] (remove #(= (:id %) product-id) products)))) 61 | 62 | (defn- get-product-ids-for 63 | "Gets a set of all product ids that belong to a particular list" 64 | [listdata] 65 | (into #{} 66 | (map :product_id 67 | (select "lists_products" 68 | (fields :product_id) 69 | (where {:list_id (:id listdata)}))))) 70 | 71 | (defn update-list [listdata] 72 | (update e/lists 73 | (set-fields (dissoc listdata :id :products)) 74 | (where {:id (:id listdata)})) 75 | (let [existing-product-ids (get-product-ids-for listdata) 76 | updated-product-ids (->> (:products listdata) 77 | (map :id) 78 | (into #{})) 79 | to-add (difference updated-product-ids existing-product-ids) 80 | to-remove (difference existing-product-ids updated-product-ids)] 81 | (doseq [prod-id to-add] 82 | (add-product listdata prod-id)) 83 | (doseq [prod-id to-remove] 84 | (remove-product listdata prod-id)) 85 | (find-by-id (:id listdata)))) 86 | 87 | (defn delete-list [listdata] 88 | (delete e/lists 89 | (where {:id (:id listdata)}))) -------------------------------------------------------------------------------- /restful-clojure/src/restful_clojure/models/products.clj: -------------------------------------------------------------------------------- 1 | (ns restful-clojure.models.products 2 | (:use korma.core) 3 | (:require [restful-clojure.entities :as e])) 4 | 5 | (defn create [product] 6 | (insert e/products 7 | (values product))) 8 | 9 | (defn find-all [] 10 | (select e/products)) 11 | 12 | (defn find-by [field value] 13 | (first 14 | (select e/products 15 | (where {field value}) 16 | (limit 1)))) 17 | 18 | (defn find-all-by [field value] 19 | (select e/products 20 | (where {field value}))) 21 | 22 | (defn find-by-id [id] 23 | (find-by :id id)) 24 | 25 | (defn count-products [] 26 | (let [agg (select e/products 27 | (aggregate (count :*) :cnt))] 28 | (get-in agg [0 :cnt] 0))) 29 | 30 | (defn update-product [product] 31 | (update e/products 32 | (set-fields (dissoc product :id)) 33 | (where {:id (product :id)}))) 34 | 35 | (defn delete-product [product] 36 | (delete e/products 37 | (where {:id (product :id)}))) -------------------------------------------------------------------------------- /restful-clojure/src/restful_clojure/models/users.clj: -------------------------------------------------------------------------------- 1 | (ns restful-clojure.models.users 2 | (:use korma.core) 3 | (:require [restful-clojure.entities :as e] 4 | [buddy.hashers :as hashers] 5 | [clojure.set :refer [map-invert]])) 6 | 7 | (def user-levels 8 | {"user" ::user 9 | "admin" ::admin}) 10 | (derive ::admin ::user) 11 | 12 | (defn- with-kw-level [user] 13 | (assoc user :level 14 | (get user-levels (:level user) ::user))) 15 | 16 | (defn- with-str-level [user] 17 | (assoc user :level (if-let [level (:level user)] 18 | (name level) 19 | "user"))) 20 | 21 | (defn find-all [] 22 | (select e/users)) 23 | 24 | (defn find-by [field value] 25 | (some-> (select* e/users) 26 | (where {field value}) 27 | (limit 1) 28 | select 29 | first 30 | (dissoc :password_digest) 31 | with-kw-level)) 32 | 33 | (defn find-by-id [id] 34 | (find-by :id id)) 35 | 36 | (defn for-list [listdata] 37 | (find-by-id (listdata :user_id))) 38 | 39 | (defn find-by-email [email] 40 | (find-by :email email)) 41 | 42 | (defn create [user] 43 | (-> (insert* e/users) 44 | (values (-> user 45 | (assoc :password_digest (hashers/encrypt (:password user))) 46 | with-str-level 47 | (dissoc :password))) 48 | insert 49 | (dissoc :password_digest) 50 | with-kw-level)) 51 | 52 | (defn update-user [user] 53 | (update e/users 54 | (set-fields (-> user 55 | (dissoc :id :password) 56 | with-str-level)) 57 | (where {:id (user :id)}))) 58 | 59 | (defn count-users [] 60 | (let [agg (select e/users 61 | (aggregate (count :*) :cnt))] 62 | (get-in agg [0 :cnt] 0))) 63 | 64 | (defn delete-user [user] 65 | (delete e/users 66 | (where {:id (user :id)}))) 67 | 68 | (defn password-matches? 69 | "Check to see if the password given matches the digest of the user's saved password" 70 | [id password] 71 | (some-> (select* e/users) 72 | (fields :password_digest) 73 | (where {:id id}) 74 | select 75 | first 76 | :password_digest 77 | (->> (hashers/check password)))) -------------------------------------------------------------------------------- /restful-clojure/test/restful_clojure/auth_test.clj: -------------------------------------------------------------------------------- 1 | (ns restful-clojure.auth-test 2 | (:use clojure.test 3 | restful-clojure.test-core) 4 | (:require [restful-clojure.auth :as auth] 5 | [restful-clojure.models.users :as u] 6 | [korma.core :as sql])) 7 | 8 | (use-fixtures :each with-rollback) 9 | 10 | (deftest authenticating-users 11 | (let [user (u/create {:name "Test" :email "user@example.com" :password "s3cr3t"})] 12 | 13 | (testing "Authenticates with valid token" 14 | (let [token (auth/make-token! (:id user))] 15 | (is (= user (auth/authenticate-token {} token))))) 16 | 17 | (testing "Does not authenticate with nonexistent token" 18 | (is (nil? (auth/authenticate-token {} "youhavetobekiddingme")))) 19 | 20 | (testing "Does not authenticate with expired token" 21 | (let [token (auth/make-token! (:id user)) 22 | sql (str "UPDATE auth_tokens " 23 | "SET created_at = NOW() - interval '7 hours' " 24 | "WHERE id = ?")] 25 | (sql/exec-raw [sql [token]]) 26 | (is (nil? (auth/authenticate-token {} token))))))) 27 | 28 | ; (detest authorizing-users 29 | ; (let [user (u/create {:name "User" :email "user@example.com" :password "s3cr3t"}) 30 | ; admin (u/create {:name "Admin" :email "admin@example.com" :password "sup3rs3cr3t" :restful-clojure.models.users/admin})])) -------------------------------------------------------------------------------- /restful-clojure/test/restful_clojure/handler_test.clj: -------------------------------------------------------------------------------- 1 | (ns restful-clojure.handler-test 2 | (:use clojure.test 3 | restful-clojure.test-core 4 | ring.mock.request 5 | restful-clojure.handler) 6 | (:require [cheshire.core :as json] 7 | [restful-clojure.models.users :as u] 8 | [restful-clojure.models.lists :as l] 9 | [restful-clojure.auth :as auth] 10 | [restful-clojure.models.products :as p])) 11 | 12 | ; WIll be rebound in test 13 | (def ^{:dynamic true} *session-id* nil) 14 | 15 | (defn with-session [t] 16 | (let [user (u/create {:name "Some admin" 17 | :email "theadmin@example.com" 18 | :password "sup3rs3cr3t" 19 | :level :restful-clojure.auth/admin}) 20 | session-id (auth/make-token! (:id user))] 21 | (with-bindings {#'*session-id* session-id} 22 | (t)) 23 | (u/delete-user user))) 24 | 25 | (use-fixtures :each with-rollback) 26 | (use-fixtures :once with-session) 27 | 28 | (defn with-auth-header [req] 29 | (header req "Authorization" (str "Token " *session-id*))) 30 | 31 | (deftest main-routes 32 | (testing "list users" 33 | (let [response (app (with-auth-header (request :get "/users")))] 34 | (is (= (:status response) 200)) 35 | (is (= (get-in response [:headers "Content-Type"]) "application/json; charset=utf-8")))) 36 | 37 | (testing "lists endpoint" 38 | (let [response (app (with-auth-header (request :get "/lists")))] 39 | (is (= (:status response) 200)) 40 | (is (= (get-in response [:headers "Content-Type"]) "application/json; charset=utf-8")))) 41 | 42 | (testing "products endpoint" 43 | (let [response (app (with-auth-header (request :get "/products")))] 44 | (is (= (:status response) 200)) 45 | (is (= (get-in response [:headers "Content-Type"]) "application/json; charset=utf-8")))) 46 | 47 | (testing "not-found route" 48 | (let [response (app (request :get "/bogus-route"))] 49 | (is (= (:status response) 404))))) 50 | 51 | (deftest creating-user 52 | (testing "POST /users" 53 | (let [user-count (u/count-users) 54 | response (app (-> (request :post "/users") 55 | with-auth-header 56 | (body (json/generate-string {:name "Joe Test" 57 | :email "joe@example.com" 58 | :password "s3cret"})) 59 | (content-type "application/json") 60 | (header "Accept" "application/json")))] 61 | (is (= (:status response) 201)) 62 | (is (substring? "/users/" (get-in response [:headers "Location"]))) 63 | (is (= (inc user-count) (u/count-users)))))) 64 | 65 | (deftest retrieve-user-stuff 66 | (let [user (u/create {:name "John Doe" :email "j.doe@mytest.com" :password "s3cret"}) 67 | initial-count (u/count-users)] 68 | 69 | (testing "GET /users" 70 | (doseq [i (range 4)] 71 | (u/create {:name "Person" :email (str "person" i "@example.com") :password "s3cret"})) 72 | (let [response (app (with-auth-header (request :get "/users"))) 73 | resp-data (json/parse-string (:body response))] 74 | (is (= (:status response 200))) 75 | ; A person's email contained in the response body 76 | (is (substring? "person3@example.com" (:body response))) 77 | ; All results present (including the user created in the let form) 78 | (is (= (+ initial-count 4) (count (get resp-data "results" [])))) 79 | ; "count" field present 80 | (is (= (+ initial-count 4) (get resp-data "count" []))))) 81 | 82 | (testing "GET /users/:id" 83 | (let [response (app (with-auth-header (request :get (str "/users/" (:id user)))))] 84 | (is (= (:body response) (json/generate-string user))))) 85 | 86 | (testing "GET /users/:id/lists" 87 | (let [my-list (l/create {:user_id (:id user) :title "Wonderful Stuffs"}) 88 | response (app (with-auth-header (request :get (str "/users/" (:id user) "/lists"))))] 89 | (is (= (:body response) (json/generate-string [(dissoc my-list :user_id)]))))))) 90 | 91 | (deftest deleting-user 92 | (let [user (u/create {:name "John Doe" :email "j.doe@mytest.com" :password "s3cr3t"})] 93 | 94 | (testing "DELETE /users/:id" 95 | (let [response (app (with-auth-header (request :delete (str "/users/" (:id user)))))] 96 | ; okay/no content status 97 | (is (= (:status response) 204)) 98 | ; redirected to users index 99 | (is (= "/users" (get-in response [:headers "Location"]))) 100 | ; user no longer exists in db 101 | (is (nil? (u/find-by-id (:id user)))))))) 102 | 103 | (deftest creating-list 104 | (testing "POST /lists" 105 | (let [list-count (l/count-lists) 106 | user (u/create {:name "John Doe" :email "j.doe@mytest.com" :password "s3cr3t"}) 107 | response (app (-> (request :post "/lists") 108 | with-auth-header 109 | (body (str "{\"user_id\":" (:id user) ",\"title\":\"Amazing Accoutrements\"}")) 110 | (content-type "application/json") 111 | (header "Accept" "application/json")))] 112 | (is (= (:status response) 201)) 113 | (is (substring? "/users/" (get-in response [:headers "Location"]))) 114 | (is (= (inc list-count) (l/count-lists)))))) 115 | 116 | (deftest retrieving-list 117 | (let [user (u/create {:name "John Doe" :email "j.doe@mytest.com" :password "s3cret"}) 118 | listdata (l/create {:user_id (:id user) :title "Root Beers of Iowa"})] 119 | 120 | (testing "GET /lists" 121 | (doseq [i (range 4)] 122 | (l/create {:user_id (:id user) :title (str "List " i)})) 123 | (let [response (app (with-auth-header (request :get "/lists"))) 124 | resp-data (json/parse-string (:body response))] 125 | (is (= (:status response 200))) 126 | ; A list title 127 | (is (substring? "List 3" (:body response))) 128 | ; All results present 129 | (is (= 5 (count (get resp-data "results" [])))) 130 | ; "count" field present 131 | (is (= 5 (get resp-data "count" []))))) 132 | 133 | (testing "GET /lists/:id" 134 | (let [response (app (with-auth-header (request :get (str "/lists/" (:id listdata)))))] 135 | (is (= (:body response) (json/generate-string listdata))))))) 136 | 137 | (deftest deleting-list 138 | (let [user (u/create {:name "John Doe" :email "j.doe@mytest.com" :password "s3cr3t"}) 139 | listdata (l/create {:user_id (:id user) :title "Root Beers of Iowa"})] 140 | 141 | (testing "DELETE /lists/:id" 142 | (let [response (app (with-auth-header (request :delete (str "/lists/" (:id listdata)))))] 143 | ; okay/no content status 144 | (is (= (:status response) 204)) 145 | ; redirected to users index 146 | (is (= "/lists" (get-in response [:headers "Location"]))) 147 | ; list no longer exists in db 148 | (is (nil? (l/find-by-id (:id listdata)))))))) 149 | 150 | (deftest creating-product 151 | (testing "POST /products" 152 | (let [prod-count (p/count-products) 153 | response (app (-> (request :post "/products") 154 | with-auth-header 155 | (body (str "{\"title\":\"Granny Smith\",\"description\":\"Howdya like them apples?\"}")) 156 | (content-type "application/json") 157 | (header "Accept" "application/json")))] 158 | (is (= (:status response) 201)) 159 | (is (substring? "/products/" (get-in response [:headers "Location"]))) 160 | (is (= (inc prod-count) (p/count-products)))))) 161 | 162 | (deftest retrieving-product 163 | (testing "GET /products" 164 | (doseq [i (range 5)] 165 | (p/create {:title (str "Product " i)})) 166 | (let [response (app (with-auth-header (request :get "/products"))) 167 | resp-data (json/parse-string (:body response))] 168 | (is (= (:status response 200))) 169 | ; Product name contained in the response body 170 | (is (substring? "Product 4" (:body response))) 171 | ; All results present 172 | (is (= 5 (count (get resp-data "results" [])))) 173 | ; "count" field present 174 | (is (= 5 (get resp-data "count" [])))))) 175 | 176 | -------------------------------------------------------------------------------- /restful-clojure/test/restful_clojure/lists_test.clj: -------------------------------------------------------------------------------- 1 | (ns restful-clojure.lists-test 2 | (:use clojure.test 3 | restful-clojure.test-core) 4 | (:require [restful-clojure.models.lists :as lists] 5 | [restful-clojure.models.products :as products] 6 | [restful-clojure.models.users :as users])) 7 | 8 | (use-fixtures :each with-rollback) 9 | 10 | (deftest create-list 11 | (let [user (users/create {:name "Test user" :email "me@mytest.com" :password "s3cret"})] 12 | 13 | (testing "Create a list for given user" 14 | (let [count-orig (lists/count-lists)] 15 | (lists/create {:user_id (:id user) :title "New Test"}) 16 | (is (= (inc count-orig) (lists/count-lists))))))) 17 | 18 | (deftest retrieve-list 19 | (let [user (users/create {:name "Test user" :email "me@mytest.com" :password "s3cr3t"})] 20 | 21 | (testing "Retrieve a list by id" 22 | (let [my-list (lists/create {:user_id (:id user) :title "My list"}) 23 | found-list (lists/find-by-id (:id my-list))] 24 | (is (= "My list" (:title found-list))) 25 | (is (= (:id user) (:user_id found-list))))))) 26 | 27 | (deftest update-list 28 | (let [user (users/create {:name "Test user" :email "me@mytest.com" :password "s3cr3t"})] 29 | 30 | (testing "Modifies existing list" 31 | (let [list-orig (lists/create {:user_id (:id user) 32 | :title "FP Languages"}) 33 | list-id (:id list-orig)] 34 | (lists/update-list (assoc list-orig :title "Awesome Languages")) 35 | (is (= "Awesome Languages" (:title (lists/find-by-id list-id)))))))) 36 | 37 | (deftest delete-lists 38 | (let [user (users/create {:name "Test user" :email "me@mytest.com" :password "s3cr3t"}) 39 | user-id (:id user)] 40 | (testing "Decreases list count" 41 | (let [listdata (lists/create {:user_id user-id :title "Unimportant things"}) 42 | list-count (lists/count-lists)] 43 | (lists/delete-list listdata) 44 | (is (= (dec list-count) (lists/count-lists))))) 45 | 46 | (testing "Deleted correct list" 47 | (let [list-keep (lists/create {:user_id user-id :title "Stuff to keep"}) 48 | list-del (lists/create {:user_id user-id :title "Stuff to delete"})] 49 | (lists/delete-list list-del) 50 | (is (seq 51 | (lists/find-by-id (:id list-keep)))) 52 | (is (nil? 53 | (lists/find-by-id (:id list-del)))))))) 54 | 55 | (deftest add-products 56 | (let [user (users/create {:name "Test user" :email "me@mytest.com" :password "s3cr3t"}) 57 | my-list (lists/create {:user_id (:id user) :title "My list"}) 58 | pintos (products/create {:title "Pinto Beans" 59 | :description "Yummy beans for burritos"})] 60 | (testing "Adds product to existing list" 61 | (let [modified-list (lists/add-product my-list (:id pintos))] 62 | (is (= [pintos] (:products modified-list))))) 63 | 64 | (testing "Creates new list with products" 65 | (let [listdata (lists/create {:user_id (:id user) 66 | :title "Most interesting" 67 | :products [pintos]})] 68 | (is (= [pintos] (:products listdata))) 69 | (is (= [pintos] (:products (lists/find-by-id (:id listdata))))))) 70 | 71 | (testing "Creates products added with an update" 72 | (let [listdata (lists/create {:user_id (:id user) 73 | :title "Things to update" 74 | :products [pintos]}) 75 | coffee (products/create {:title "Coffee Beans" 76 | :description "No, not *THAT* Java"}) 77 | updated (lists/update-list (update-in listdata [:products] conj coffee))] 78 | (is (= [pintos coffee] (:products updated))) 79 | (is (= [pintos coffee] (:products (lists/find-by-id (:id listdata))))))))) 80 | 81 | (deftest remove-products 82 | (let [user (users/create {:name "Test user" :email "me@mytest.com" :password "s3cr3t"}) 83 | kidneys (products/create {:title "Kidney Beans" 84 | :description "Poor Charlie the Unicorn..."}) 85 | limas (products/create {:title "Lima Beans" 86 | :description "Yuck!"}) 87 | my-list (lists/create {:user_id (:id user) 88 | :title "My list" 89 | :products [kidneys limas]})] 90 | (testing "Does not remove a product from the database entirely" 91 | (let [fresh-list (lists/create {:user_id (:id user) 92 | :title "My list" 93 | :products [kidneys limas]})] 94 | (lists/remove-product fresh-list (:id kidneys)) 95 | (is (not (nil? (products/find-by-id (:id kidneys))))))) 96 | 97 | (testing "Removes a product from a list" 98 | (let [modified-list (lists/remove-product my-list (:id kidneys))] 99 | (is (= [limas] (:products modified-list))))) 100 | 101 | (testing "Removes products absent from an update" 102 | (let [coffee (products/create {:title "Coffee Beans" 103 | :description "No, not *THAT* Java"}) 104 | listdata (lists/create {:user_id (:id user) 105 | :title "Things to update" 106 | :products [limas coffee]}) 107 | updated (lists/update-list (assoc listdata :products [coffee]))] 108 | (is (= [coffee] (:products updated))) 109 | (is (= [coffee] (:products (lists/find-by-id (:id listdata))))))))) 110 | -------------------------------------------------------------------------------- /restful-clojure/test/restful_clojure/products_test.clj: -------------------------------------------------------------------------------- 1 | (ns restful-clojure.products-test 2 | (:use clojure.test 3 | restful-clojure.test-core) 4 | (:require [restful-clojure.models.products :as products])) 5 | 6 | (use-fixtures :each with-rollback) 7 | 8 | (deftest create-product 9 | (testing "Create a product increments product count" 10 | (let [count-orig (products/count-products)] 11 | (products/create {:title "Cherry Tomatos" 12 | :description "Tasty red tomatos"}) 13 | (is (= (inc count-orig) (products/count-products)))))) -------------------------------------------------------------------------------- /restful-clojure/test/restful_clojure/test_core.clj: -------------------------------------------------------------------------------- 1 | ;; Common utilities to use across tests 2 | (ns restful-clojure.test-core 3 | (:use clojure.test) 4 | (:require [korma.db :as db])) 5 | 6 | (defn substring? [sub st] 7 | (if (nil? st) 8 | false 9 | (not= (.indexOf st sub) -1))) 10 | 11 | (defn with-rollback 12 | "Test fixture for executing a test inside a database transaction 13 | that rolls back at the end so that database tests can remain isolated" 14 | [test-fn] 15 | (db/transaction 16 | (test-fn) 17 | (db/rollback))) 18 | -------------------------------------------------------------------------------- /restful-clojure/test/restful_clojure/users+lists_test.clj: -------------------------------------------------------------------------------- 1 | (ns restful-clojure.users+lists-test 2 | (:use clojure.test 3 | restful-clojure.test-core) 4 | (:require [restful-clojure.models.users :as users] 5 | [restful-clojure.models.lists :as lists] 6 | [environ.core :refer [env]])) 7 | 8 | ; Run each test in an isolated db transaction and rollback 9 | ; afterwards 10 | (use-fixtures :each with-rollback) 11 | 12 | (deftest user-list-interactions 13 | (let [user (users/create {:name "Jed" :email "jed@i.com" :password "s3cr3t"}) 14 | my-list (lists/create {:user_id (user :id) 15 | :title "Ways to use the force"})] 16 | (testing "Get user for given list" 17 | (is (= (users/find-by-id (user :id)) 18 | (users/for-list my-list)))) 19 | 20 | (testing "Get all lists for user" 21 | (is (= 1 (count (lists/for-user user)))) 22 | (lists/create {:user_id (user :id) 23 | :title "Lightsaber wishlist"}) 24 | (is (= 2 (count (lists/for-user user))))))) 25 | 26 | (deftest cascading-operations 27 | (testing "Deleting user removes associated lists" 28 | (let [jack (users/create {:name "Jack" 29 | :email "beanstalkz@example.com" 30 | :password "s3cr3t"}) 31 | tmp-list (lists/create {:user_id (jack :id) 32 | :title "Talking points"})] 33 | (users/delete-user jack) 34 | (is (= nil (lists/find-by-id (tmp-list :id))))))) -------------------------------------------------------------------------------- /restful-clojure/test/restful_clojure/users_test.clj: -------------------------------------------------------------------------------- 1 | (ns restful-clojure.users-test 2 | (:use clojure.test 3 | restful-clojure.test-core) 4 | (:require [restful-clojure.models.users :as users] 5 | [restful-clojure.entities :as e] 6 | [korma.core :as sql] 7 | [environ.core :refer [env]])) 8 | 9 | ; Run each test in an isolated db transaction and rollback 10 | ; afterwards 11 | (use-fixtures :each with-rollback) 12 | 13 | (deftest create-read-users 14 | (testing "Create user" 15 | (let [count-orig (users/count-users)] 16 | (users/create {:name "Charlie" :email "charlie@example.com" :password "foo"}) 17 | (is (= (inc count-orig) (users/count-users))))) 18 | 19 | (testing "Retrieve user" 20 | (let [user (users/create {:name "Andrew" :email "me@mytest.com" :password "foo"}) 21 | found-user (users/find-by-id (user :id))] 22 | (is (= "Andrew" (found-user :name)) 23 | (is (= "me@mytest.com" (found-user :email)))))) 24 | 25 | (testing "Find by email" 26 | (users/create {:name "John Doe" :email "j.doe@ihearttractors.com" :password "foo"}) 27 | (let [user (users/find-by-email "j.doe@ihearttractors.com")] 28 | (is (= "John Doe" (user :name))))) 29 | 30 | (testing "Create with user level" 31 | (let [admin (users/create {:name "Jane Doe" :email "jane@ihearttractors.com" :password "foo" :level "admin"}) 32 | expected-level :restful-clojure.models.users/admin] 33 | (is (= expected-level (:level admin))) 34 | (is (= expected-level (:level (users/find-by-id (:id admin)))))))) 35 | 36 | (deftest multiple-user-operations 37 | (testing "Find all users" 38 | (doseq [i (range 10)] 39 | (users/create {:name "Test user" 40 | :email (str "user." i "@example.com") 41 | :password "foo"})) 42 | (is (= 10 (count (users/find-all)))))) 43 | 44 | (deftest update-users 45 | (testing "Modifies existing user" 46 | (let [user-orig (users/create {:name "Curious George" :email "i.go.bananas@hotmail.com" :password "foo"}) 47 | user-id (user-orig :id)] 48 | (users/update-user (assoc user-orig :name "Chiquita Banana")) 49 | (is (= "Chiquita Banana" (:name (users/find-by-id user-id))))))) 50 | 51 | (deftest delete-users 52 | (testing "Decreases user count" 53 | (let [user (users/create {:name "Temporary" :email "ephemerial@shortlived.org" :password "foo"}) 54 | user-count (users/count-users)] 55 | (users/delete-user user) 56 | (is (= (dec user-count) (users/count-users))))) 57 | 58 | (testing "Deleted correct user" 59 | (let [user-keep (users/create {:name "Keep" :email "important@users.net" :password "foo"}) 60 | user-del (users/create {:name "Delete" :email "irrelevant@users.net" :password "foo"})] 61 | (users/delete-user user-del) 62 | (is (= (dissoc user-keep :password) 63 | (users/find-by-id (user-keep :id)))) 64 | (is (nil? 65 | (users/find-by-id (user-del :id))))))) 66 | 67 | (deftest authenticate-users 68 | (let [user (users/create {:name "Sly" :email "sly@falilystone.com" :password "s3cr3t"}) 69 | user-id (:id user)] 70 | (testing "Accepts the correct password" 71 | (is (users/password-matches? user-id "s3cr3t"))) 72 | 73 | (testing "Rejects incorrect passwords" 74 | (is (not (users/password-matches? user-id "not_my_password")))) 75 | 76 | (testing "Does not store the password in plain text" 77 | (let [stored-pass (:password_digest (first (sql/select e/users 78 | (sql/where {:email "sly@falilystone.com"}))))] 79 | (is (not= stored-pass "s3cr3t")))) 80 | 81 | (testing "Does not include password or digest on user response" 82 | (let [user (users/find-by-id user-id)] 83 | (is (nil? (:password user))) 84 | (is (nil? (:password_digest user))))))) 85 | --------------------------------------------------------------------------------