Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 27 additions & 28 deletions src/boostbox/boostbox.clj
Original file line number Diff line number Diff line change
Expand Up @@ -236,33 +236,34 @@
;; provided by us
#_[:id {:optional true} :string]
;; provided by boost client
[:action [:enum :boost :stream]]
[:action {:decode/json str/lower-case
:decode/string str/lower-case} [:enum "boost" "stream"]]
[:split {:json-schema/default 1.0} [:double {:min 0.0}]]
[:value_msat {:json-schema/default 2222000} [:int {:min 1}]]
[:value_msat_total {:json-schema/default 2222000} [:int {:min 1}]]
[:timestamp {:json-schema/default (java.time.Instant/now)}
[:and :string
[:fn {:error/message "must be ISO-8601"} valid-iso8601?]]]
;; optional keys
[:group {:optional true} :string]
[:message {:optional true :json-schema/default "row of ducks"} :string]
[:app_name {:optional true} :string]
[:app_version {:optional true} :string]
[:sender_id {:optional true} :string]
[:sender_name {:optional true} :string]
[:recipient_name {:optional true} :string]
[:recipient_address {:optional true} :string]
[:value_usd {:optional true} [:double {:min 0.0}]]
[:position {:optional true} :int]
[:feed_guid {:optional true} :string]
[:feed_title {:optional true} :string]
[:item_guid {:optional true} :string]
[:item_title {:optional true} :string]
[:publisher_guid {:optional true} :string]
[:publisher_title {:optional true} :string]
[:remote_feed_guid {:optional true} :string]
[:remote_item_guid {:optional true} :string]
[:remote_publisher_guid {:optional true} :string]])
;; optional keys
[:group {:optional true} [:maybe :string]]
[:message {:optional true :json-schema/default "row of ducks"} [:maybe :string]]
[:app_name {:optional true} [:maybe :string]]
[:app_version {:optional true} [:maybe :string]]
[:sender_id {:optional true} [:maybe :string]]
[:sender_name {:optional true} [:maybe :string]]
[:recipient_name {:optional true} [:maybe :string]]
[:recipient_address {:optional true} [:maybe :string]]
[:value_usd {:optional true} [:maybe [:double {:min 0.0}]]]
[:position {:optional true} [:maybe :int]]
[:feed_guid {:optional true} [:maybe :string]]
[:feed_title {:optional true} [:maybe :string]]
[:item_guid {:optional true} [:maybe :string]]
[:item_title {:optional true} [:maybe :string]]
[:publisher_guid {:optional true} [:maybe :string]]
[:publisher_title {:optional true} [:maybe :string]]
[:remote_feed_guid {:optional true} [:maybe :string]]
[:remote_item_guid {:optional true} [:maybe :string]]
[:remote_publisher_guid {:optional true} [:maybe :string]]])

;; ~~~~~~~~~~~~~~~~~~~ GET View ~~~~~~~~~~~~~~~~~~~
(defn encode-header [data]
Expand Down Expand Up @@ -292,7 +293,7 @@
(let [boost-id (get data "id")
json-pretty (json/write-value-as-string data (json/object-mapper {:pretty true}))
sender-name (get data "sender_name")
value-msats (get data "value_msat")
value-msats (get data "value_msat_total")
sats (format-sats value-msats)
feed-title (get data "feed_title")
item-title (get data "item_title")
Expand Down Expand Up @@ -390,7 +391,7 @@
(str "rss::payment::" action " " url)))

(defn add-boost [cfg storage]
(fn [{:keys [:body-params] :as request}]
(fn [{{body-params :body} :parameters :as request}]
(let [id (gen-ulid)
url (str (:base-url cfg) "/boost/" id)
boost (assoc body-params :id id)
Expand Down Expand Up @@ -489,6 +490,7 @@
:coercion (reitit.coercion.malli/create
{:error-keys #{:in :humanized}
:compile mu/open-schema
:strip-extra-keys false
:default-values true})
:middleware [swagger/swagger-feature
parameters/parameters-middleware
Expand Down Expand Up @@ -531,14 +533,11 @@
(assoc-in response [:headers "x-correlation-id"] correlation-id))))

(defn mulog-wrapper [handler]
(fn [{:keys [:request-method :uri :query-params :path-params :body-params :correlation-id] :as request}]
(fn [{:keys [:request-method :uri :correlation-id] :as request}]
(u/trace ::http-request
{:pairs [:correlation-id correlation-id
:method request-method
:uri uri
:query-params query-params
:path-params path-params
:body-params body-params]
:uri uri]
:capture (fn [{:keys [:status ::exception] :as response}]
(let [success (< status 400)
base {:status status
Expand Down
78 changes: 78 additions & 0 deletions test/boostbox/boostbox_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,84 @@
;; Check we received our sent payload plus id.
(is (= expected decoded-json))))))))))))


(deftest test-oscar-fountain-boost
(run-with-storage
["FS" "S3"]
(fn [data]
(testing "Oscar's real Fountain boost - extra fields, nulls, and case handling"
(let [base-url (-> data :config :base-url)
api-key (-> data :config :allowed-keys first)
;; Oscar's actual boost with extras and nulls
oscar-boost {:action "BOOST" ; uppercase to test normalization
:split 0.05
:message "Test Boost 2 👀👀👀👀👀👀"
:link "https://fountain.fm/episode/JCIzq3VyFKQVkEzVNA8v?payment=5XIAt66P29Iv6rjSTZUB"
:app_name "Fountain"
:sender_id "hIWsCYxdBJzlDvu5zpT3"
:sender_name "merryoscar@fountain.fm"
:sender_npub "npub1unmftuzmkpdjxyj4en8r63cm34uuvjn9hnxqz3nz6fls7l5jzzfqtvd0j2"
:recipient_address "ericpp@getalby.com"
:value_msat 50000
:value_usd 0.049998
:value_msat_total 1000000
:timestamp "2025-11-07T14:36:23.861Z"
:position 5192
:feed_guid "917393e3-1b1e-5cef-ace4-edaa54e1f810"
:feed_title "Podcasting 2.0"
:item_guid "PC20-240"
:item_title "Episode 240: Open Source = People!"
:publisher_guid nil
:publisher_title nil
:remote_feed_guid nil
:remote_item_guid nil
:remote_publisher_guid nil}

post-resp (http/post (str base-url "/boost")
{:headers {"x-api-key" api-key
"Content-Type" "application/json"}
:body (json/write-value-as-string oscar-boost)
:throw false})]

(is (= 201 (:status post-resp)) "Should accept Oscar's boost")

(let [post-body (json/read-value (:body post-resp))
boost-id (get post-body "id")]
(when boost-id
(let [
boost-url (get post-body "url")
get-resp (http/get boost-url {:throw false})
header (get-in get-resp [:headers "x-rss-payment"])
decoded (-> header
(java.net.URLDecoder/decode "UTF-8")
(json/read-value))]

(is (= 200 (:status get-resp)) "GET should return 200")

;; Verify normalization
(is (= "boost" (get decoded "action"))
"Action should be lowercased from BOOST")

;; Verify extra fields pass through (not in schema)
(is (= "https://fountain.fm/episode/JCIzq3VyFKQVkEzVNA8v?payment=5XIAt66P29Iv6rjSTZUB"
(get decoded "link"))
"Extra field 'link' should be preserved")
(is (= "npub1unmftuzmkpdjxyj4en8r63cm34uuvjn9hnxqz3nz6fls7l5jzzfqtvd0j2"
(get decoded "sender_npub"))
"Extra field 'sender_npub' should be preserved")

;; Verify null handling
(is (nil? (get decoded "publisher_guid"))
"Null values should be preserved")
(is (nil? (get decoded "remote_feed_guid"))
"Multiple null fields should be preserved")

;; Verify HTML renders without errors
(is (str/includes? (:body get-resp) "Boost Details")
"HTML view should render successfully")
(is (str/includes? (:body get-resp) "Test Boost 2")
"HTML should show the message")))))))))

;; --- Unhappy Path Smoke Tests ---

(deftest smoke-test-413-payload-too-large
Expand Down