├── .travis.yml ├── test-resources └── default.edn ├── .gitignore ├── project.clj ├── test └── de │ └── otto │ └── tesla │ ├── util │ └── test_utils.clj │ └── mongo │ └── mongo_test.clj ├── README.md ├── src └── de │ └── otto │ └── tesla │ └── mongo │ └── mongo.clj └── LICENSE /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | -------------------------------------------------------------------------------- /test-resources/default.edn: -------------------------------------------------------------------------------- 1 | { 2 | :default-mongo-port 27018 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | *.out 9 | /.lein-* 10 | /.nrepl-port 11 | /.idea 12 | *iml 13 | log_location_IS_UNDEFINED/ 14 | embongo.log 15 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject de.otto/tesla-mongo-connect "0.3.5-SNAPSHOT" 2 | :description "Addon to https://github.com/otto-de/tesla-microservice to read and write to mongodb." 3 | :url "https://github.com/otto-de/tesla-mongo-connect" 4 | :license {:name "Apache License 2.0" 5 | :url "http://www.apache.org/license/LICENSE-2.0.html"} 6 | :scm {:name "git" 7 | :url "https://github.com/otto-de/tesla-mongo-connect"} 8 | :dependencies [[org.clojure/clojure "1.9.0"] 9 | [com.novemberain/monger "3.1.0"] 10 | [de.otto/goo "1.2.4"]] 11 | 12 | :plugins [[lein-embongo "0.2.2"]] 13 | 14 | :aliases {"test" ["do" "embongo" "test"]} 15 | :embongo {:port 27018 16 | :version "2.6.4" 17 | :data-dir "./target/mongo-data-files"} 18 | 19 | :lein-release {:deploy-via :clojars} 20 | :profiles {:provided {:dependencies [[de.otto/tesla-microservice "0.11.17"]]} 21 | :dev {:plugins [[lein-release/lein-release "1.0.9"]]}} 22 | 23 | :source-paths ["src"] 24 | :test-paths ["test" "test-resources"]) 25 | -------------------------------------------------------------------------------- /test/de/otto/tesla/util/test_utils.clj: -------------------------------------------------------------------------------- 1 | (ns de.otto.tesla.util.test-utils 2 | (:require [clojure.test :refer :all] 3 | [com.stuartsierra.component :as comp])) 4 | 5 | (defmacro with-started 6 | "bindings => [name init ...] 7 | 8 | Evaluates body in a try expression with names bound to the values 9 | of the inits after (comp/start init) has been called on them. Finally 10 | a clause calls (comp/stop name) on each name in reverse order." 11 | [bindings & body] 12 | (if (and 13 | (vector? bindings) "a vector for its binding" 14 | (even? (count bindings)) "an even number of forms in binding vector") 15 | (cond 16 | (= (count bindings) 0) `(do ~@body) 17 | (symbol? (bindings 0)) `(let [~(bindings 0) (comp/start ~(bindings 1))] 18 | (try 19 | (with-started ~(subvec bindings 2) ~@body) 20 | (finally 21 | (comp/stop ~(bindings 0))))) 22 | :else (throw (IllegalArgumentException. 23 | "with-started-system only allows Symbols in bindings"))) 24 | (throw (IllegalArgumentException. 25 | "not a vector or bindings-count is not even")))) 26 | 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tesla-mongo-connect 2 | 3 | An addon to [tesla-microservice](https://github.com/otto-de/tesla-microservice) 4 | that allows to read and write to mongodb. 5 | 6 | [![Clojars Project](http://clojars.org/de.otto/tesla-mongo-connect/latest-version.svg)](http://clojars.org/de.otto/tesla-mongo-connect) 7 | 8 | [![Build Status](https://travis-ci.org/otto-de/tesla-mongo-connect.svg)](https://travis-ci.org/otto-de/tesla-mongo-connect) 9 | [![Dependencies Status](http://jarkeeper.com/otto-de/tesla-mongo-connect/status.svg)](http://jarkeeper.com/otto-de/tesla-mongo-connect) 10 | 11 | ## Usage 12 | 13 | You can initialize several instances of tesla-mongo-connect in your tesla-microservice. In the simple case each one connects to a single database on a mongodb. The configurations for the individual connections are distinguished by prefix: 14 | 15 | 16 | ``` 17 | test-db1.mongo.host=localhost 18 | test-db1.mongo.dbname=teslatest 19 | test-db1.mongo.user= 20 | test-db1.mongo.passwd= 21 | test-db.mongo.socket-timeout=30 22 | test-db.mongo.socket-keep-alive=true 23 | test-db.mongo.connection-timeout=2000 24 | 25 | 26 | test-db2.mongo.host=other-url 27 | test-db2.mongo.dbname=teslaprod 28 | test-db2.mongo.user=user 29 | test-db2.mongo.passwd=passwd 30 | test-db2.mongo.socket-timeout=42 31 | test-db2.mongo.connection-timeout=3000 32 | ``` 33 | 34 | Now you can establish two connections like this: 35 | 36 | ```clojure 37 | (defn example-system [runtime-config] 38 | (-> (system/empty-system (merge {:name "mongo-example-service"} runtime-config)) 39 | (assoc :mongo1 40 | (c/using (mongo/new-mongo "test-db1") [:config :metering :app-status])) 41 | (assoc :mongo2 42 | (c/using (mongo/new-mongo "test-db2") [:config :metering :app-status])) 43 | (assoc :foo (foo/new-foo) [:mongo1 :mongo2]) 44 | (c/system-using {:server [:example-page]}))) 45 | ``` 46 | 47 | In the component ```:foo``` you could then find the document with the id of "foo" in the collection "my-collection" in the database ```:mongo1``` like this: 48 | 49 | ```clojure 50 | (mongo/find-one-checked! (:mongo1 self) "my-collection" {:_id "foo"}) 51 | ``` 52 | 53 | 54 | For a working example see [the mongo-example](https://github.com/otto-de/tesla-examples/tree/master/mongo-example). in _tesla-examples_. 55 | 56 | ## TODO 57 | * Add description for (optional) dbname switching functionality 58 | 59 | 60 | ## Initial Contributors 61 | 62 | Christian Stamm, Kai Brandes, Daley Chetwynd, Felix Bechstein, Ralf Sigmund, Florian Weyandt 63 | 64 | ## License 65 | 66 | Apache License 67 | -------------------------------------------------------------------------------- /src/de/otto/tesla/mongo/mongo.clj: -------------------------------------------------------------------------------- 1 | (ns de.otto.tesla.mongo.mongo 2 | (:require [com.stuartsierra.component :as component] 3 | [monger.core :as mg] 4 | [monger.query :as mq] 5 | [monger.collection :as mc] 6 | [clojure.tools.logging :as log] 7 | [clojure.string :as str] 8 | [de.otto.tesla.stateful.app-status :as app-status] 9 | [de.otto.status :as s] 10 | [de.otto.goo.goo :as goo]) 11 | (:import com.mongodb.ReadPreference 12 | (com.mongodb MongoException MongoCredential MongoClient))) 13 | 14 | (defn property-for-db [conf which-db property-name] 15 | (get conf (keyword (str which-db "-mongo-" (name property-name))))) 16 | 17 | (defn parse-port [conf prop] 18 | (Integer. 19 | (if-let [port (prop "port")] 20 | port 21 | (:default-mongo-port conf)))) 22 | 23 | (defn parse-server-address [conf prop] 24 | (let [host (prop "host") 25 | port (parse-port conf prop)] 26 | (if (.contains host ",") 27 | (map #(mg/server-address % port) (str/split host #",")) 28 | (mg/server-address host port)))) 29 | 30 | (def read-preference {:primary-preferred (ReadPreference/primaryPreferred) 31 | :primary (ReadPreference/primary) 32 | :secondary-preferred (ReadPreference/secondaryPreferred) 33 | :secondary (ReadPreference/secondary) 34 | :nearest (ReadPreference/nearest)}) 35 | 36 | (defn default-options [prop] 37 | {:socket-timeout (if-let [st (prop :socket-timeout)] 38 | (read-string st) 39 | 31) 40 | :connect-timeout (if-let [ct (prop :connect-timeout)] 41 | (read-string ct) 42 | 2000) 43 | :connections-per-host (or (prop :max-connections-per-host) 100) 44 | :min-connections-per-host (or (prop :min-connections-per-host) 0) 45 | :socket-keep-alive (= "true" (prop :socket-keep-alive)) 46 | :threads-allowed-to-block-for-connection-multiplier 30 47 | :read-preference ((or (prop :read-preference) :secondary-preferred) read-preference)}) 48 | 49 | (defn read-timer-name [db] 50 | (str "mongo." db ".read")) 51 | 52 | (defn prop-resolution-fun [prop] 53 | (log/info "choosing prop-dbname-resolution-fun to determine dbname") 54 | (let [from-property (prop "dbname")] 55 | (fn [] from-property))) 56 | 57 | (defn resolve-db-name [self] 58 | ((:dbname-fun self))) 59 | 60 | (defn create-mongo-credential [prop dbname] 61 | (let [user (prop "user") 62 | password (prop "passwd")] 63 | (if (not (str/blank? user)) 64 | [(MongoCredential/createCredential user dbname (.toCharArray password))] 65 | []))) 66 | 67 | (defn create-client-options [prop] 68 | (let [options (default-options prop) 69 | options-builder (mg/mongo-options-builder options) 70 | options-builder (.minConnectionsPerHost options-builder (:min-connections-per-host options))] 71 | (.build options-builder))) 72 | 73 | (defn create-client [conf prop dbname] 74 | (let [server-address (parse-server-address conf prop) 75 | cred (create-mongo-credential prop dbname) 76 | options (create-client-options prop)] 77 | (MongoClient. server-address cred options))) 78 | 79 | 80 | (defn authenticated-db [conf prop dbname] 81 | (try 82 | (.getDB (create-client conf prop dbname) dbname) 83 | (catch MongoException e 84 | (log/error e "error authenticating mongo-connection") 85 | :not-connected))) 86 | 87 | (defn nil-if-not-connected [db] 88 | (if (= :not-connected db) 89 | nil 90 | db)) 91 | 92 | (defn new-db-connection [dbNamesToConns conf prob dbname] 93 | (log/info "initializing new connection for db-name " dbname) 94 | (let [db (authenticated-db conf prob dbname)] 95 | (swap! dbNamesToConns #(assoc % dbname db)) 96 | (nil-if-not-connected db))) 97 | 98 | (defn db-by-name [self dbname] 99 | (if-let [db (get @(:dbNamesToConns self) dbname)] 100 | (nil-if-not-connected db) 101 | (new-db-connection (:dbNamesToConns self) (:conf self) (:prop self) dbname))) 102 | 103 | (defn status-fun [self] 104 | (s/status-detail 105 | (keyword (str "mongo-" (:which-db self))) 106 | :ok 107 | "mongo" 108 | {:active-dbs (keys @(:dbNamesToConns self)) 109 | :current-db (resolve-db-name self)})) 110 | 111 | (defprotocol DbNameLookup 112 | (dbname-lookup-fun [self])) 113 | 114 | (defrecord Mongo [which-db config app-status dbname-lookup] 115 | component/Lifecycle 116 | (start [self] 117 | (log/info (str "-> starting mongodb " which-db)) 118 | (let [conf (:config config) 119 | prop (partial property-for-db conf which-db) 120 | new-self (assoc self 121 | :conf conf 122 | :prop prop 123 | :dbNamesToConns (atom {}) 124 | :dbname-fun (if (nil? dbname-lookup) 125 | (prop-resolution-fun prop) 126 | (dbname-lookup-fun dbname-lookup)))] 127 | (app-status/register-status-fun app-status (partial status-fun new-self)) 128 | (new-db-connection (:dbNamesToConns new-self) conf prop ((:dbname-fun new-self))) 129 | new-self)) 130 | 131 | (stop [self] 132 | (log/info "<- stopping mongodb") 133 | (map (fn [_ v] (mg/disconnect v)) @(:dbNamesToConns self)) 134 | self)) 135 | 136 | (defn current-db [self] 137 | (db-by-name self (resolve-db-name self))) 138 | 139 | (defn- clear! 140 | "removes everything from a collection. Only for tests." 141 | [self col] 142 | (if (not (and (.contains col "test") (.contains (resolve-db-name self) "test"))) 143 | (throw (IllegalArgumentException. "won't clear"))) 144 | (mc/remove (current-db self) col) 145 | :ok) 146 | 147 | 148 | (defmacro timed [body command] 149 | `(goo/timed :mongo/duration-in-s {:command ~command} ~body [0.001 0.005 0.01 0.05 0.1])) 150 | 151 | (defn update-upserting! 152 | [self col query doc] 153 | (timed (mc/update (current-db self) col query doc {:upsert true}) :upsert)) 154 | 155 | (defn find-one! 156 | ([self col query] 157 | (find-one! self col query [])) 158 | ([self col query fields] 159 | (log/debugf "mongodb query: %s %s %s" col query fields) 160 | (timed (some-> (current-db self) 161 | (mc/find-one-as-map col query fields)) :find-one))) 162 | 163 | (defn find-one-checked! 164 | ([self col query] 165 | (find-one-checked! self col query [])) 166 | ([self col query fields] 167 | (try 168 | (find-one! self col query fields) 169 | (catch MongoException e 170 | (log/warn e "mongo-exception for query: " query))))) 171 | 172 | (defn find! [self col query fields] 173 | (log/debugf "mongodb query: %s %s" col query) 174 | (timed (some-> (current-db self) (mc/find-maps col query fields)) :find)) 175 | 176 | (defn find-checked! 177 | ([self col query] (find-checked! self col query [])) 178 | ([self col query fields] 179 | (try 180 | (find! self col query fields) 181 | (catch MongoException e 182 | (log/warn e "mongo-exception for query: " query))))) 183 | 184 | (defn count! [self col query] 185 | (log/debugf "mongodb count: %s %s" col query) 186 | (timed (some-> (current-db self) 187 | (mc/count col query)) :count)) 188 | 189 | (defn count-checked! [self col query] 190 | (try 191 | (count! self col query) 192 | (catch MongoException e 193 | (log/warn e "mongo-exception for query: " query)))) 194 | 195 | (defn remove-by-id! 196 | [self col id] 197 | (timed (mc/remove-by-id (current-db self) col id) :remove)) 198 | 199 | (defn find-ordered [self col query order limit] 200 | (timed (mq/exec 201 | (-> (mq/empty-query (.getCollection (current-db self) col)) 202 | (mq/find query) 203 | (mq/sort order) 204 | (mq/limit limit))) :find-ordered)) 205 | 206 | (defn insert! 207 | [self col doc] 208 | (timed (mc/insert-and-return (current-db self) col doc) :insert)) 209 | 210 | (defn new-mongo 211 | ([which-db] (map->Mongo {:which-db which-db}))) 212 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /test/de/otto/tesla/mongo/mongo_test.clj: -------------------------------------------------------------------------------- 1 | (ns de.otto.tesla.mongo.mongo-test 2 | (:require [clojure.test :refer :all] 3 | [com.stuartsierra.component :as c] 4 | [de.otto.tesla.mongo.mongo :as mongo] 5 | [monger.core :as mg] 6 | [de.otto.tesla.util.test-utils :as u] 7 | [de.otto.tesla.system :as system]) 8 | (:import (com.mongodb MongoException DB ReadPreference MongoClientOptions))) 9 | 10 | (defn mongo-test-system [config which-db] 11 | (-> (system/base-system config) 12 | (assoc :mongo (c/using (mongo/new-mongo which-db) 13 | [:config :app-status])) 14 | (dissoc :server))) 15 | 16 | ;; import private method. 17 | (def clear-collection! (ns-resolve 'de.otto.tesla.mongo.mongo 'clear!)) 18 | 19 | ;###################################################################################### 20 | 21 | (deftest should-construct-timer-name 22 | (testing "should construct correct timer name for graphite" 23 | (is (= (mongo/read-timer-name "a.db.name") "mongo.a.db.name.read")))) 24 | 25 | (deftest ^:unit should-read-correct-properties 26 | (testing "should create host property" 27 | (let [which-db "foodb" 28 | config {:default-mongo-port 27017 29 | :foodb-mongo-host "foohost"}] 30 | (is (= "foohost" (mongo/property-for-db config which-db "host")))) 31 | 32 | (testing "should create host property" 33 | (let [which-db "foodb" 34 | config {:default-mongo-port 27017 35 | :foodb-mongo-host "foohost,blahost"} 36 | prop (partial mongo/property-for-db config which-db) 37 | hosts (mongo/parse-server-address config prop)] 38 | (is (= [(mg/server-address "foohost") (mg/server-address "blahost")] hosts))))) 39 | 40 | (testing "should create host property" 41 | (let [which-db "foodb" 42 | config {:foodb-mongo-dbname "foodb"}] 43 | (is (= "foodb" (mongo/property-for-db config which-db "dbname")))))) 44 | 45 | (deftest ^:unit should-create-correct-host-property-for-multiple-host-names 46 | (let [conf {:default-mongo-port 27017 47 | :foodb-mongo-host "foohost,blahost"} 48 | prop (partial mongo/property-for-db conf "foodb") 49 | hosts (mongo/parse-server-address conf prop)] 50 | (is (= [(mg/server-address "foohost") (mg/server-address "blahost")] hosts)))) 51 | 52 | (deftest ^:unit should-create-correct-timer-names 53 | (is (= (mongo/read-timer-name "a.db.name") "mongo.a.db.name.read"))) 54 | 55 | (deftest ^:unit should-read-correct-host-and-db-properties 56 | (let [config {:foodb-mongo-host "foohost" 57 | :foodb-mongo-dbname "foodb"}] 58 | 59 | (testing "should create host property" 60 | (is (= "foohost" (mongo/property-for-db config "foodb" "host")))) 61 | 62 | (testing "should create host property" 63 | (is (= "foodb" (mongo/property-for-db config "foodb" "dbname")))))) 64 | 65 | 66 | 67 | (defrecord FoodbNameLookup [] 68 | mongo/DbNameLookup 69 | (dbname-lookup-fun [_] (fn [] "foodb")) 70 | c/Lifecycle 71 | (start [self] self) 72 | (stop [self] self)) 73 | 74 | 75 | (defn test-system-with-lookup [] 76 | (-> (system/base-system {}) 77 | (assoc :dbname-lookup (FoodbNameLookup.)) 78 | (assoc :mongo (c/using (mongo/new-mongo "prod") 79 | [:config :app-status :dbname-lookup])) 80 | (dissoc :server))) 81 | 82 | (deftest ^:unit should-use-a-provided-dbname-fn 83 | (testing "it uses the provided function" 84 | (with-redefs [mongo/new-db-connection (fn [_ _ _ _] "bar")] 85 | (u/with-started [started (test-system-with-lookup)] 86 | (is (= ((:dbname-fun (:mongo started))) "foodb")))))) 87 | (deftest ^:integration clearing-does-not-work-on-production-data 88 | (u/with-started [started (mongo-test-system {:prod-mongo-host "localhost" 89 | :prod-mongo-dbname "valuable-production-data"} 90 | "prod")] 91 | (is (thrown? IllegalArgumentException 92 | (clear-collection! (:mongo started) "this-data-is-worth-its-weight-in-gold"))))) 93 | 94 | (deftest ^:integration clearing-works-on-testdata 95 | (u/with-started [started (mongo-test-system {:prod-mongo-host "localhost" 96 | :prod-mongo-dbname "invaluable-test-data"} 97 | "prod")] 98 | (is (= :ok 99 | (clear-collection! (:mongo started) "this-test-data-is-not-worth-its-weight-in-floppy-disks"))))) 100 | 101 | (deftest ^:integration writing-and-reading-a-simple-cowboy 102 | (u/with-started [started (mongo-test-system {:cowboys-mongo-host "localhost" 103 | :cowboys-mongo-dbname "test-cowboy-db"} "cowboys")] 104 | (let [mongo (:mongo started) 105 | collection "test-cowboys"] 106 | (clear-collection! mongo collection) 107 | (let [written (mongo/insert! mongo collection 108 | {:name "Bill" :occupation "Cowboy"}) 109 | read (mongo/find-one-checked! mongo collection 110 | {:_id (:_id written)})] 111 | (is (= (:name read) "Bill")))))) 112 | 113 | (deftest ^:integration counting-entries-in-a-collection 114 | (u/with-started [started (mongo-test-system {:cowboys-mongo-host "localhost" 115 | :cowboys-mongo-dbname "test-cowboy-db"} "cowboys")] 116 | (testing "unchecked count" 117 | (let [mongo (:mongo started) 118 | collection "test-cowboys"] 119 | (clear-collection! mongo collection) 120 | (is (= (mongo/count! mongo collection {}) 0)) 121 | (mongo/insert! mongo collection 122 | {:name "Bill" :occupation "Cowboy"}) 123 | (is (= (mongo/count! mongo collection {}) 1)) 124 | (is (= (mongo/count! mongo collection {:name "Bill"}) 1)) 125 | (is (= (mongo/count! mongo collection {:name "Eddy"}) 0)) 126 | )) 127 | (testing "checked count" 128 | (let [mongo (:mongo started) 129 | collection "test-cowboys"] 130 | (clear-collection! mongo collection) 131 | (mongo/insert! mongo collection 132 | {:name "Bill" :occupation "Cowboy"}) 133 | (is (= (mongo/count-checked! mongo collection {}) 1)) 134 | )) 135 | (testing "should catch exception from mongo" 136 | (let [mongo (:mongo started) 137 | collection "test-cowboys"] 138 | (with-redefs [mongo/count! (fn [& _] (throw (MongoException. "some exception")))] 139 | (is (= nil 140 | (mongo/count-checked! mongo collection {}))) ;; Look ma no exception 141 | ))))) 142 | 143 | (deftest ^:integration finding-documents-by-array-entry 144 | (u/with-started [started (mongo-test-system {:pseudonym-mongo-host "localhost" 145 | :pseudonym-mongo-dbname "test-pseudonym-db"} "pseudonym")] 146 | (let [mongo (:mongo started) 147 | col "test-pseudonyms"] 148 | (clear-collection! (:mongo started) "test-pseudonyms") 149 | (mongo/insert! mongo col {:_id "someOtherId" 150 | :visitors ["abc" "def"] 151 | :orderedVariations [] 152 | :lastAccess #inst "2014-10-18T22:32:06.899-00:00" 153 | :v "1.0"}) 154 | (is (= "someOtherId" 155 | (:_id (mongo/find-one-checked! mongo col {:visitors "abc"}))))))) 156 | 157 | (deftest ^:unit should-not-throw-any-exception-if-authentication-fails 158 | (with-redefs-fn {#'mongo/create-client (fn [_ _ _] (throw (MongoException. "some exception")))} 159 | #(u/with-started [started (mongo-test-system {:default-mongo-port 27017 160 | :foodb-mongo-dbname "foo-db" 161 | :foodb-mongo-host "foohost"} "foodb")] 162 | (is @(:dbNamesToConns (:mongo started)) 163 | {"foodb" :not-connected})))) 164 | 165 | (deftest ^:unit should-set-min-connections-per-host 166 | (testing "Setting the min connections per host" 167 | (let [^MongoClientOptions options (mongo/create-client-options {:min-connections-per-host 1})] 168 | (is (= 1 (.getMinConnectionsPerHost options))))) 169 | (testing "Default is zero" 170 | (let [^MongoClientOptions options (mongo/create-client-options {})] 171 | (is (= 0 (.getMinConnectionsPerHost options)))))) 172 | 173 | (deftest ^:unit should-set-max-connections-per-host 174 | (testing "Setting the min connections per host" 175 | (let [^MongoClientOptions options (mongo/create-client-options {:max-connections-per-host 1})] 176 | (is (= 1 (.getConnectionsPerHost options))))) 177 | (testing "Default is 100" 178 | (let [^MongoClientOptions options (mongo/create-client-options {})] 179 | (is (= 100 (.getConnectionsPerHost options)))))) 180 | 181 | (deftest ^:integration should-add-db-id-everything-is-fine 182 | (u/with-started [started (mongo-test-system {:default-mongo-port 27017 183 | :foodb-mongo-dbname "foo-db" 184 | :foodb-mongo-host "localhost"} "foodb")] 185 | (println (class (get @(:dbNamesToConns (:mongo started)) "foo-db"))) 186 | (is (= (class (get @(:dbNamesToConns (:mongo started)) "foo-db")) 187 | DB)))) 188 | 189 | (deftest find-one-checked-test 190 | (with-redefs [mongo/find-one! (fn [_ _ _ fields] fields)] 191 | (testing "Should pass field arguments" 192 | (is (= ["my.nested.field"] 193 | (mongo/find-one-checked! {} "col" {} ["my.nested.field"])))) 194 | (testing "Should pass empty field arguments in case none passed in" 195 | (is (= [] 196 | (mongo/find-one-checked! {} "col" {})))))) 197 | 198 | (deftest find-checked-test 199 | (with-redefs [mongo/find! (fn [_ _ _ fields] fields)] 200 | (testing "Should pass field arguments" 201 | (is (= ["my.nested.field"] 202 | (mongo/find-checked! {} "col" {} ["my.nested.field"])))) 203 | (testing "Should pass empty field arguments in case none passed in" 204 | (is (= [] 205 | (mongo/find-checked! {} "col" {})))))) 206 | 207 | (deftest ^:unit test-default-options 208 | (testing "default values" 209 | (let [conf {} 210 | prop (partial mongo/property-for-db conf "testdb") 211 | options (mongo/default-options prop)] 212 | (is (= 31 213 | (options :socket-timeout))) 214 | (is (= 2000 215 | (options :connect-timeout))) 216 | (is (= false 217 | (options :socket-keep-alive))) 218 | (is (= (ReadPreference/secondaryPreferred) 219 | (options :read-preference))))) 220 | 221 | (testing "default values can be configured with properties per db" 222 | (let [conf {:testdb-mongo-socket-timeout "42" 223 | :testdb-mongo-socket-keep-alive "true"} 224 | prop (partial mongo/property-for-db conf "testdb") 225 | options (mongo/default-options prop)] 226 | (is (= 42 227 | (options :socket-timeout))) 228 | (is (= true 229 | (options :socket-keep-alive))))) 230 | 231 | (testing "should choose read preference from config" 232 | (let [conf {:testdb-mongo-socket-timeout "42" 233 | :testdb-mongo-socket-keep-alive "true" 234 | :testdb-mongo-read-preference :primary-preferred} 235 | prop (partial mongo/property-for-db conf "testdb") 236 | options (mongo/default-options prop)] 237 | (is (= (ReadPreference/primaryPreferred) 238 | (options :read-preference)))))) 239 | --------------------------------------------------------------------------------