service-ize
CI / Test + coverage (push) Successful in 27s

This commit is contained in:
2026-09-19 13:57:13 -09:00
parent 5aaf5a8d6c
commit 1f907cfe7b
7 changed files with 206 additions and 85 deletions
+15 -24
View File
@@ -211,40 +211,31 @@
(is (= 200 (:status (app {:request-method :get :uri "/api/add" :query-string "x=2&y=3"})))))))
(deftest todo-tools-test
;; The rules (ownership, validation, not-found) are tbd.todo.service's and tested there; this is
;; the tools' side: arguments in, JSON out, the caller's identity passed on, errors as results.
(let [{:keys [app alice bob]} (fixture)
alice-sid (initialize app alice)
bob-sid (initialize app bob)
call (fn [token sid tool arguments] (call-tool app token sid tool arguments))
as-alice (partial call alice alice-sid)
as-bob (partial call bob bob-sid)
result (fn [resp] (:result resp))
as-alice (partial call-tool app alice alice-sid)
as-bob (partial call-tool app bob bob-sid)
data (fn [resp] (m/decode "application/json" (text resp)))
error (fn [resp] (when (true? (get-in resp [:result :isError])) (text resp)))]
(testing "no todos yet"
(is (= [] (data (as-alice "list_todos" {})))))
(is (= [] (data (as-alice "list_todos" {}))))
(let [created (data (as-alice "create_todo" {:title " write tests "}))
id (:id created)]
(testing "create trims the title, as the API does, and returns the todo"
(is (= {:title "write tests" :done false :owner-id 7} (select-keys created [:title :done :owner-id])))
(is (integer? id)))
(testing "get and list"
(is (= created (data (as-alice "get_todo" {:id id}))))
(is (= [created] (data (as-alice "list_todos" {})))))
(testing "update changes the title and/or done"
(is (= {:title "write tests" :done true} (select-keys (data (as-alice "update_todo" {:id id :done true})) [:title :done])))
(is (= {:title "ship it" :done true} (select-keys (data (as-alice "update_todo" {:id id :title "ship it"})) [:title :done]))))
(testing "todos are private to their owner, and someone else's is reported as missing"
(is (= {:title "write tests" :done false :owner-id 7} (select-keys created [:title :done :owner-id])))
(is (= created (data (as-alice "get_todo" {:id id}))))
(is (= [created] (data (as-alice "list_todos" {}))))
(is (= {:title "ship it" :done true}
(select-keys (data (as-alice "update_todo" {:id id :title "ship it" :done true})) [:title :done])))
(testing "the tools act as whoever the token belongs to"
(is (= [] (data (as-bob "list_todos" {}))))
(doseq [[tool args] [["get_todo" {:id id}] ["update_todo" {:id id :done false}] ["delete_todo" {:id id}]]]
(is (= "todo not found" (error (as-bob tool args))) tool))
(is (= "ship it" (:title (data (as-alice "get_todo" {:id id})))) "and bob's attempts changed nothing"))
(testing "the rules of tbd.todo.core apply here too"
(is (= "todo not found" (error (as-bob "get_todo" {:id id})))))
(testing "a service error is an error result the model can read"
(is (= "title must not be blank" (error (as-alice "create_todo" {:title " "}))))
(is (= "nothing to change: give a title and/or done" (error (as-alice "update_todo" {:id id})))))
(testing "delete"
(is (= "deleted" (text (as-alice "delete_todo" {:id id}))))
(is (= "todo not found" (error (as-alice "get_todo" {:id id}))))
(is (= [] (data (as-alice "list_todos" {}))))))))
(is (= "deleted" (text (as-alice "delete_todo" {:id id}))))
(is (= "todo not found" (error (as-alice "get_todo" {:id id})))))))
(deftest tool-arguments-are-validated-test
(let [{:keys [app alice]} (fixture)
+75
View File
@@ -0,0 +1,75 @@
(ns tbd.todo.service-test
(: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.todo.service :as service]))
;; The rules every front end (HTTP routes, MCP tools) gets: ownership, not-found and validation.
;; They are tested here once; the front ends' tests only cover their own translation.
(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))))
(def ^:private not-found {:error "todo not found" :kind :not-found})
(deftest lifecycle-test
(let [ds (fresh-ds)]
(is (= {:ok []} (service/list-todos ds alice)))
(let [{created :ok} (service/create! ds alice " buy milk ")
id (:id created)]
(testing "create trims the title, makes it the user's and returns it stored"
(is (= {:owner-id 7 :title "buy milk" :done false} (select-keys created [:owner-id :title :done])))
(is (integer? id)))
(is (= {:ok created} (service/find-todo ds alice id)))
(is (= {:ok [created]} (service/list-todos ds alice)))
(testing "update changes the title and/or done"
(is (= {:title "buy milk" :done true}
(select-keys (:ok (service/update! ds alice id {:done true})) [:title :done])))
(is (= {:title "oat milk" :done true}
(select-keys (:ok (service/update! ds alice id {:title "oat milk"})) [:title :done])))
(is (= "oat milk" (:title (:ok (service/find-todo ds alice id)))) "and it is stored"))
(testing "delete"
(is (= {:ok true} (service/delete! ds alice id)))
(is (= not-found (service/find-todo ds alice id)))
(is (= not-found (service/delete! ds alice id)))
(is (= {:ok []} (service/list-todos ds alice)))))))
(deftest validation-test
(let [ds (fresh-ds)
id (:id (:ok (service/create! ds alice "keep")))]
(testing "the rules of tbd.todo.core apply, as :invalid"
(is (= {:error "title must not be blank" :kind :invalid} (service/create! ds alice " ")))
(is (= {:error (format "title must be at most %d characters" todo/max-title-length) :kind :invalid}
(service/create! ds alice (apply str (repeat (inc todo/max-title-length) "x")))))
(is (= {:error "title must not be blank" :kind :invalid} (service/update! ds alice id {:title " "})))
(is (= {:error "nothing to change: give a title and/or done" :kind :invalid}
(service/update! ds alice id {}))))
(is (= ["keep"] (map :title (:ok (service/list-todos ds alice)))) "nothing above changed anything")))
(deftest ownership-test
(let [ds (fresh-ds)
{mine :ok} (service/create! ds alice "alice's")
id (:id mine)]
(testing "each user has their own list"
(service/create! ds bob "bob's")
(is (= ["alice's"] (map :title (:ok (service/list-todos ds alice)))))
(is (= ["bob's"] (map :title (:ok (service/list-todos ds bob))))))
(testing "someone else's todo is reported exactly like a missing one"
(let [missing (service/find-todo ds bob 999999)]
(is (= not-found missing))
(is (= missing (service/find-todo ds bob id)))
(is (= missing (service/update! ds bob id {:title "hijacked"})))
(is (= missing (service/delete! ds bob id)))))
(is (= {:ok mine} (service/find-todo ds alice id)) "and alice's todo is untouched")))
(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)))]
(is (zero? @opened))
(service/list-todos ds alice)
(service/list-todos ds alice)
(is (= 1 @opened)))))