merge db sources
CI / Test + coverage (push) Successful in 46s

This commit is contained in:
2026-09-21 14:11:00 -09:00
parent bf38f7b1be
commit 187252177c
21 changed files with 168 additions and 153 deletions
+4 -5
View File
@@ -15,10 +15,9 @@
(str/ends-with? url "/api/v1/user") {:status 200 :body (format "{\"login\":\"%s\",\"id\":%d}" login id)}))))
(defn make-app
"The app against a Gitea stub. `:todo`, `:oauth` and `:chat` (their configs) are passed through; other overrides go into the auth config."
[http & {:keys [todo oauth chat] :as overrides}]
(core/make-app {:todo todo
:oauth oauth
"The app against a Gitea stub. `:db` and `:chat` (their configs) are passed through; other overrides go into the auth config."
[http & {:keys [db chat] :as overrides}]
(core/make-app {:db db
:chat chat
:auth (merge {:base-url "http://tbd.test"
:gitea-url "https://gitea.test"
@@ -29,7 +28,7 @@
:session-secret "test-secret"
:secure-cookies? false
:http http}
(dissoc overrides :todo :oauth :chat))
(dissoc overrides :db :chat))
:nrepl {:port 0 :bind "127.0.0.1"}}))
(defn request [app method uri & {:keys [cookie query]}]
+2 -2
View File
@@ -5,7 +5,7 @@
[tbd.core :as core]
[tbd.http :as http]
[tbd.test-util :as test-util]
[tbd.todo.db-test :as db-test])
[tbd.sqlite-test :as sqlite-test])
(:import (java.io ByteArrayInputStream)))
(defn call
@@ -65,7 +65,7 @@
(is (= "application/edn; charset=utf-8" (content-type (call app :get "/api/todos" {}))))))))
(deftest request-format-test
(let [app (t/make-app (t/gitea-stub "alice" 7) :todo {:db-path (db-test/temp-db-path)})
(let [app (t/make-app (t/gitea-stub "alice" 7) :db {:db-path (sqlite-test/temp-db-path)})
cookie (t/login! app)
post (fn [headers body] (call app :post "/api/todos" (assoc headers "cookie" cookie) body))]
(testing "JSON in, EDN out by default"
+7 -7
View File
@@ -6,7 +6,7 @@
[malli.core :as malli]
[tbd.oauth.core :as oauth]
[tbd.oauth.db :as db]
[tbd.oauth.db-test :as db-test]
[tbd.sqlite-test :as sqlite-test]
[tbd.oauth.routes-test :as o]
[tbd.auth.routes-test :as t])
(:import (java.io ByteArrayInputStream)))
@@ -80,9 +80,9 @@
(defn- fixture
"An app on a fresh database, with the tokens of an admin (alice) and a regular user (bob)."
[]
(let [path (db-test/temp-db-path)]
(let [path (sqlite-test/temp-db-path)]
{:path path
:app (t/make-app (t/gitea-stub "alice" 7) :oauth {:db-path path} :todo {:db-path (db-test/temp-db-path)})
:app (t/make-app (t/gitea-stub "alice" 7) :db {:db-path path})
:alice (mint-token! path "alice" 7)
:bob (mint-token! path "bob" 8)}))
@@ -177,22 +177,22 @@
(is (= 401 (:status resp)))))))
(deftest admin-follows-the-allowlist-test
(let [path (db-test/temp-db-path)
(let [path (sqlite-test/temp-db-path)
token (mint-token! path "alice" 7)
app-with (fn [admins] (t/make-app (t/gitea-stub "alice") :oauth {:db-path path} :admin-users admins))]
app-with (fn [admins] (t/make-app (t/gitea-stub "alice") :db {:db-path path} :admin-users admins))]
(is (= "alice (Gitea user 7), admin" (text (call-tool (app-with #{"alice"}) token (initialize (app-with #{"alice"}) token) "whoami" {}))))
(testing "it is decided per request, not when the token was issued"
(let [app (app-with #{})]
(is (= "alice (Gitea user 7)" (text (call-tool app token (initialize app token) "whoami" {}))))))))
(deftest unconfigured-test
(let [app (t/make-app (t/gitea-stub "alice") :oauth {:db-path (db-test/temp-db-path)} :gitea-url nil)
(let [app (t/make-app (t/gitea-stub "alice") :db {:db-path (sqlite-test/temp-db-path)} :gitea-url nil)
resp (rpc app "anything" {:jsonrpc "2.0" :id 1 :method "initialize"})]
(is (= 503 (:status resp)) "with no login there is no way to get a token, so /mcp isn't open instead")))
(deftest end-to-end-test
(testing "a client that does the whole OAuth dance ends up as the user who approved it"
(let [app (o/app-for (db-test/temp-db-path) "carol" 9)
(let [app (o/app-for (sqlite-test/temp-db-path) "carol" 9)
{:keys [client-id tokens]} (o/get-tokens! app)
whoami (fn [access-token] (text (call-tool app access-token (initialize app access-token) "whoami" {})))]
(is (= "carol (Gitea user 9)" (whoami (:access_token tokens))))
+2 -11
View File
@@ -2,12 +2,8 @@
(:require [clojure.java.io :as io]
[clojure.test :refer [deftest is testing]]
[next.jdbc :as jdbc]
[tbd.oauth.db :as db])
(:import (java.nio.file Files)
(java.nio.file.attribute FileAttribute)))
(defn temp-db-path []
(str (Files/createTempDirectory "tbd-oauth" (make-array FileAttribute 0)) "/data/oauth.db"))
[tbd.oauth.db :as db]
[tbd.sqlite-test :refer [temp-db-path]]))
(defn client [id created-at]
{:id id :name "Test client" :redirect-uris ["https://c.example.com/cb" "http://localhost/cb"] :created-at created-at})
@@ -18,11 +14,6 @@
:refresh-hash (str "refresh-" n) :refresh-expires-at 9000
:created-at 1000})
(deftest env-config-test
(is (= {:db-path "data/oauth.db"} (db/env-config {})))
(is (= {:db-path "data/oauth.db"} (db/env-config {"OAUTH_DB_PATH" " "})))
(is (= {:db-path "/var/oauth.db"} (db/env-config {"OAUTH_DB_PATH" " /var/oauth.db "}))))
(deftest open-test
(testing "creates the file and its directory"
(let [path (temp-db-path)]
+12 -12
View File
@@ -6,7 +6,7 @@
[tbd.auth.routes-test :as t]
[tbd.oauth.core :as oauth]
[tbd.oauth.db :as db]
[tbd.oauth.db-test :as db-test]
[tbd.sqlite-test :as sqlite-test]
[tbd.test-util :as test-util])
(:import (java.io ByteArrayInputStream)
(java.net URLDecoder)))
@@ -23,7 +23,7 @@
(defn app-for
"An app whose OAuth database is `path`; the Gitea user is alice (id 7), an admin, unless told otherwise."
([path] (app-for path "alice" 7))
([path login id] (t/make-app (t/gitea-stub login id) :oauth {:db-path path})))
([path login id] (t/make-app (t/gitea-stub login id) :db {:db-path path})))
(defn call
"Runs a request through `app`. `:form` (a map) is sent as a urlencoded body, `:body` (a string) as JSON."
@@ -94,7 +94,7 @@
{:client-id client-id :cookie cookie :tokens (:body (token! app client-id code))}))
(deftest discovery-test
(let [app (app-for (db-test/temp-db-path))]
(let [app (app-for (sqlite-test/temp-db-path))]
(testing "protected resource metadata, at the root and at the resource's own path"
(doseq [uri ["/.well-known/oauth-protected-resource" "/.well-known/oauth-protected-resource/mcp"]]
(let [resp (call app :get uri)]
@@ -112,8 +112,8 @@
[:headers "Content-Type"]))))))
(deftest unconfigured-test
(let [path (db-test/temp-db-path)
app (t/make-app (t/gitea-stub "alice") :oauth {:db-path path} :gitea-url nil)]
(let [path (sqlite-test/temp-db-path)
app (t/make-app (t/gitea-stub "alice") :db {:db-path path} :gitea-url nil)]
(doseq [[method uri] [[:get "/.well-known/oauth-protected-resource"]
[:get "/.well-known/oauth-authorization-server"]
[:post "/oauth/register"]
@@ -123,7 +123,7 @@
(is (= 503 (:status (call app method uri))) (str method " " uri)))))
(deftest register-test
(let [path (db-test/temp-db-path)
(let [path (sqlite-test/temp-db-path)
app (app-for path)]
(testing "a client is registered as a public client"
(let [resp (call app :post "/oauth/register"
@@ -168,7 +168,7 @@
(is (= error (get-in resp [:body :error])) why))))))
(deftest authorize-request-errors-test
(let [app (app-for (db-test/temp-db-path))
(let [app (app-for (sqlite-test/temp-db-path))
client-id (register! app)
cookie (t/login! app)
open (fn [params] (call app :get "/oauth/authorize" :cookie cookie :query (auth/form-encode params)))]
@@ -206,7 +206,7 @@
:query (str "client_id=" client-id "&client_id=" client-id))))))))
(deftest login-then-consent-test
(let [app (app-for (db-test/temp-db-path))
(let [app (app-for (sqlite-test/temp-db-path))
client-id (register! app)
query (auth/form-encode (authorize-params client-id))]
(testing "a logged-out user is sent to log in first, and back afterwards"
@@ -224,7 +224,7 @@
(is (str/includes? (:body page) "Test client")))))))))
(deftest consent-page-test
(let [app (app-for (db-test/temp-db-path))
(let [app (app-for (sqlite-test/temp-db-path))
client-id (register! app {:client_name "<script>alert(1)</script>"})
{:keys [page csrf]} (consent! app (t/login! app) (authorize-params client-id))]
(is (= 200 (:status page)))
@@ -243,7 +243,7 @@
(is (= "no-store" (get-in page [:headers "Cache-Control"]))))))
(deftest consent-decision-test
(let [app (app-for (db-test/temp-db-path))
(let [app (app-for (sqlite-test/temp-db-path))
client-id (register! app)
params (authorize-params client-id)
cookie (t/login! app)]
@@ -282,7 +282,7 @@
(is (nil? (get-in resp [:headers "Location"])))))))
(deftest code-exchange-test
(let [path (db-test/temp-db-path)
(let [path (sqlite-test/temp-db-path)
app (app-for path)
client-id (register! app)
other-client-id (register! app)
@@ -340,7 +340,7 @@
"the token endpoint takes forms, as RFC 6749 says"))))
(deftest refresh-test
(let [path (db-test/temp-db-path)
(let [path (sqlite-test/temp-db-path)
app (app-for path)
{:keys [client-id tokens]} (get-tokens! app)
refresh! (fn [refresh-token & {:as overrides}]
+44
View File
@@ -0,0 +1,44 @@
(ns tbd.sqlite-test
(:require [clojure.java.io :as io]
[clojure.test :refer [deftest is testing]]
[next.jdbc :as jdbc]
[tbd.oauth.db :as oauth-db]
[tbd.sqlite :as sqlite]
[tbd.todo.db :as todo-db])
(:import (java.nio.file Files)
(java.nio.file.attribute FileAttribute)))
(defn temp-db-path
"A path for a database file inside a fresh temp directory (which `open` has to create a
subdirectory of, as with the default data/tbd.db)."
[]
(str (Files/createTempDirectory "tbd" (make-array FileAttribute 0)) "/data/tbd.db"))
(deftest env-config-test
(is (= {:db-path "data/tbd.db"} (sqlite/env-config {})))
(is (= {:db-path "data/tbd.db"} (sqlite/env-config {"DB_PATH" " "})))
(is (= {:db-path "/var/tbd.db"} (sqlite/env-config {"DB_PATH" " /var/tbd.db "}))))
(deftest open-test
(testing "creates the file and its directory"
(let [path (temp-db-path)]
(sqlite/open path todo-db/migrate!)
(is (.exists (io/file path)))))
(testing "no path is an error, not a database in the working directory"
(is (thrown? clojure.lang.ExceptionInfo (sqlite/open nil)))
(is (thrown? clojure.lang.ExceptionInfo (sqlite/open " "))))
(testing "every migration runs on the datasource, and again (harmlessly) on reopening"
(let [path (temp-db-path)
tables #(set (map :name (jdbc/execute! % ["SELECT name FROM sqlite_master WHERE type = 'table'"]
{:builder-fn next.jdbc.result-set/as-unqualified-maps})))]
(is (every? (tables (sqlite/open path todo-db/migrate! oauth-db/migrate!))
["todos" "clients" "codes" "grants"]))
(is (every? (tables (sqlite/open path todo-db/migrate! oauth-db/migrate!))
["todos" "clients" "codes" "grants"]))))
(testing "the groups keep their data apart in the one file"
(let [path (temp-db-path)
ds (sqlite/open path todo-db/migrate! oauth-db/migrate!)]
(todo-db/insert! ds {:owner-id 7 :title "buy milk" :done false :created-at 1 :updated-at 1})
(oauth-db/insert-client! ds {:id "c1" :name "Test client" :redirect-uris ["http://localhost/cb"] :created-at 1})
(is (= ["buy milk"] (map :title (todo-db/list-todos ds 7))))
(is (= "c1" (:id (oauth-db/find-client ds "c1")))))))
+2 -14
View File
@@ -2,24 +2,12 @@
(:require [clojure.java.io :as io]
[clojure.test :refer [deftest is testing]]
[next.jdbc :as jdbc]
[tbd.todo.db :as db])
(:import (java.nio.file Files)
(java.nio.file.attribute FileAttribute)))
(defn temp-db-path
"A path for a database file inside a fresh temp directory (which `open` has to create a
subdirectory of, as with the default data/todo.db)."
[]
(str (Files/createTempDirectory "tbd-todo" (make-array FileAttribute 0)) "/data/todo.db"))
[tbd.sqlite-test :refer [temp-db-path]]
[tbd.todo.db :as db]))
(defn todo [owner-id title]
{:owner-id owner-id :title title :done false :created-at 1000 :updated-at 1000})
(deftest env-config-test
(is (= {:db-path "data/todo.db"} (db/env-config {})))
(is (= {:db-path "data/todo.db"} (db/env-config {"TODO_DB_PATH" " "})))
(is (= {:db-path "/var/todo.db"} (db/env-config {"TODO_DB_PATH" " /var/todo.db "}))))
(deftest open-test
(testing "creates the file and its directory"
(let [path (temp-db-path)]
+6 -6
View File
@@ -3,7 +3,7 @@
[clojure.test :refer [deftest is testing]]
[tbd.auth.routes-test :as t]
[tbd.test-util :as test-util]
[tbd.todo.db-test :as db-test])
[tbd.sqlite-test :as sqlite-test])
(:import (java.io ByteArrayInputStream)))
(defn request
@@ -15,12 +15,12 @@
(test-util/decode-body resp)))
(defn app-for [db-path login id]
(t/make-app (t/gitea-stub login id) :todo {:db-path db-path}))
(t/make-app (t/gitea-stub login id) :db {:db-path db-path}))
(def ^:private todos "/api/todos")
(deftest anonymous-test
(let [path (db-test/temp-db-path)
(let [path (sqlite-test/temp-db-path)
app (app-for path "alice" 7)]
(doseq [[method uri body] [[:get todos nil]
[:post todos "{\"title\":\"x\"}"]
@@ -34,7 +34,7 @@
(is (not (.exists (io/file path))) "the database isn't even opened")))
(deftest lifecycle-test
(let [app (app-for (db-test/temp-db-path) "alice" 7)
(let [app (app-for (sqlite-test/temp-db-path) "alice" 7)
cookie (t/login! app)]
(testing "starts empty"
(let [resp (request app :get todos cookie)]
@@ -65,7 +65,7 @@
(is (= ["walk dog"] (map :title (:todos (:body (request app :get todos cookie)))))))))))
(deftest validation-test
(let [app (app-for (db-test/temp-db-path) "alice" 7)
(let [app (app-for (sqlite-test/temp-db-path) "alice" 7)
cookie (t/login! app)
created (:body (request app :post todos cookie "{\"title\":\"keep\"}"))
one (str todos "/" (:id created))]
@@ -86,7 +86,7 @@
(is (= [created] (:todos (:body (request app :get todos cookie)))) "nothing above changed the todo")))
(deftest per-user-scoping-test
(let [path (db-test/temp-db-path)
(let [path (sqlite-test/temp-db-path)
alice-app (app-for path "alice" 7)
bob-app (app-for path "bob" 8)
alice (t/login! alice-app)
+3 -3
View File
@@ -2,7 +2,7 @@
(:require [clojure.test :refer [deftest is testing]]
[tbd.todo.core :as todo]
[tbd.todo.db :as db]
[tbd.todo.db-test :as db-test]
[tbd.sqlite-test :as sqlite-test]
[tbd.todo.service :as service]))
;; The rules every front end (HTTP routes, MCP tools) gets: ownership, not-found and validation.
@@ -11,7 +11,7 @@
(def alice {:id 7 :login "alice"})
(def bob {:id 8 :login "bob"})
(defn- fresh-ds [] (let [path (db-test/temp-db-path)] (delay (db/open path))))
(defn- fresh-ds [] (let [path (sqlite-test/temp-db-path)] (delay (db/open path))))
(def ^:private not-found {:error "todo not found" :kind :not-found})
@@ -68,7 +68,7 @@
(deftest database-is-opened-on-use-test
(testing "nothing opens the database until a service fn needs it"
(let [opened (atom 0)
ds (delay (swap! opened inc) (db/open (db-test/temp-db-path)))]
ds (delay (swap! opened inc) (db/open (sqlite-test/temp-db-path)))]
(is (zero? @opened))
(service/list-todos ds alice)
(service/list-todos ds alice)