Files
atomsync/test/pocketbook/server_test.clj
Florian Schroedl 570a087f53 refactor: move TodoMVC example into example/ directory
- example/todomvc/pocketbook/todomvc.cljs — app source
- example/todomvc/todomvc.html — page
- example/todomvc/js/ — compiled output (gitignored)

Server static file serving is now filesystem-based via --static-dir
flag instead of classpath resources. No static dir by default (API
only); bb server passes --static-dir example/todomvc.

Removed resources/public/ — library has no bundled static assets.
2026-04-04 17:05:12 +02:00

143 lines
5.5 KiB
Clojure

(ns pocketbook.server-test
(:require [clojure.test :refer [deftest is testing use-fixtures]]
[pocketbook.server :as server]
[pocketbook.transit :as t])
(:import [java.io File]
[java.net URI]
[java.net.http HttpClient HttpRequest HttpResponse$BodyHandlers HttpRequest$BodyPublishers]))
(def ^:dynamic *server* nil)
(def ^:dynamic *port* nil)
(defn- free-port []
(with-open [s (java.net.ServerSocket. 0)]
(.getLocalPort s)))
(use-fixtures :each
(fn [f]
(let [port (free-port)
db-path (str (File/createTempFile "pocketbook-server-test" ".db"))
srv (server/start! {:port port :db-path db-path})]
(Thread/sleep 200) ;; let server start
(try
(binding [*server* srv *port* port]
(f))
(finally
(server/stop! srv)
(.delete (File. db-path)))))))
;; ---------------------------------------------------------------------------
;; HTTP helpers
;; ---------------------------------------------------------------------------
(def ^:private client (HttpClient/newHttpClient))
(defn- url [path & [query]]
(str "http://localhost:" *port* path (when query (str "?" query))))
(defn- get-transit [path query]
(let [req (-> (HttpRequest/newBuilder)
(.uri (URI. (url path query)))
(.header "Accept" "application/transit+json")
(.GET)
(.build))
resp (.send client req (HttpResponse$BodyHandlers/ofByteArray))]
{:status (.statusCode resp)
:body (t/decode (.body resp))}))
(defn- post-transit [path body]
(let [bytes (t/encode body)
req (-> (HttpRequest/newBuilder)
(.uri (URI. (url path)))
(.header "Content-Type" "application/transit+json")
(.header "Accept" "application/transit+json")
(.POST (HttpRequest$BodyPublishers/ofByteArray bytes))
(.build))
resp (.send client req (HttpResponse$BodyHandlers/ofByteArray))]
{:status (.statusCode resp)
:body (t/decode (.body resp))}))
;; ---------------------------------------------------------------------------
;; Tests
;; ---------------------------------------------------------------------------
(deftest root-returns-404-without-static-dir
(let [req (-> (HttpRequest/newBuilder)
(.uri (URI. (url "/")))
(.GET)
(.build))
resp (.send client req (HttpResponse$BodyHandlers/ofString))]
(is (= 404 (.statusCode resp)))))
(deftest push-and-pull
(testing "Push new documents"
(let [resp (post-transit "/sync"
[{:id "todo:1" :value {:text "Buy milk" :tags #{:groceries}} :base-version 0}
{:id "todo:2" :value {:text "Buy eggs"} :base-version 0}])]
(is (= 200 (:status resp)))
(is (every? #(= :ok (:status %)) (:body resp)))
(is (= 1 (:version (first (:body resp)))))))
(testing "Pull all docs"
(let [resp (get-transit "/sync" "group=todo&since=0")]
(is (= 200 (:status resp)))
(is (= 2 (count (:body resp))))
(is (= #{:groceries} (:tags (:value (first (:body resp))))))))
(testing "Update a doc"
(let [resp (post-transit "/sync"
[{:id "todo:1" :value {:text "Buy oat milk"} :base-version 1}])]
(is (= :ok (:status (first (:body resp)))))
(is (= 2 (:version (first (:body resp)))))))
(testing "Pull only recent changes"
(let [all (get-transit "/sync" "group=todo&since=0")
ts (:updated (second (:body all)))
recent (get-transit "/sync" (str "group=todo&since=" ts))]
;; Should get only todo:1 (updated) but not todo:2 (unchanged since ts)
;; (depends on timing, but at minimum we get at least 1)
(is (<= (count (:body recent)) 2)))))
(deftest push-conflict
(post-transit "/sync"
[{:id "todo:1" :value {:text "v1"} :base-version 0}])
(let [resp (post-transit "/sync"
[{:id "todo:1" :value {:text "stale"} :base-version 0}])]
(is (= :conflict (:status (first (:body resp)))))
(is (= 1 (:current-version (first (:body resp)))))))
(deftest push-delete
(post-transit "/sync"
[{:id "todo:del" :value {:text "delete me"} :base-version 0}])
(let [resp (post-transit "/sync"
[{:id "todo:del" :deleted true :base-version 1}])]
(is (= :ok (:status (first (:body resp))))))
(let [resp (get-transit "/sync" "group=todo&since=0")]
(is (some #(and (= "todo:del" (:id %)) (:deleted %)) (:body resp)))))
(deftest missing-group-param
(let [resp (get-transit "/sync" "since=0")]
(is (= 400 (:status resp)))))
(deftest type-preservation-over-wire
(let [uuid (java.util.UUID/randomUUID)
inst #inst "2026-04-04T10:00:00Z"
value {:keyword :hello
:set #{1 2 3}
:vec [1 "two" :three]
:uuid uuid
:inst inst
:nested {:a {:b 42}}}]
(post-transit "/sync"
[{:id "types:1" :value value :base-version 0}])
(let [resp (get-transit "/sync" "group=types&since=0")
pulled (:value (first (:body resp)))]
;; Transit preserves most types but nippy is used server-side
;; The round-trip is: transit-decode → nippy-freeze → nippy-thaw → transit-encode
(is (= :hello (:keyword pulled)))
(is (= #{1 2 3} (:set pulled)))
(is (= [1 "two" :three] (:vec pulled)))
(is (= uuid (:uuid pulled)))
(is (= inst (:inst pulled)))
(is (= {:a {:b 42}} (:nested pulled))))))