diff --git a/src/boostbox/boostbox.clj b/src/boostbox/boostbox.clj index 4b86c5c..da4b013 100644 --- a/src/boostbox/boostbox.clj +++ b/src/boostbox/boostbox.clj @@ -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] @@ -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") @@ -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) @@ -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 @@ -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 diff --git a/test/boostbox/boostbox_test.clj b/test/boostbox/boostbox_test.clj index 88cd684..41165bb 100644 --- a/test/boostbox/boostbox_test.clj +++ b/test/boostbox/boostbox_test.clj @@ -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