How do I make this test pass:
(ns imp-rest.parser-test-rest
(:require [clojure.test :refer :all])
(:require [ring.mock.request :as mock] )
(:require [imp-rest.web :as w]))
(deftest test-parser-rest
(testing "put settings"
(w/app
(mock/request :put "/settings/coordinateName" "FOO" ))
(let [response (w/app (mock/request :get "/settings"))]
(println response )
(is (= (get (:body response) :coordinateName) "FOO")))))
it fails with:
FAIL in (test-parser-rest) (parser_test_rest.clj:30)
put settings
expected: (= (get (:body response) :coordinateName) "FOO")
actual: (not (= nil "FOO"))
Here's my handler:
(ns imp-rest.web
(:use compojure.core)
(:use ring.middleware.json-params)
(:require [clj-json.core :as json])
(:require [ring.util.response :as response])
(:require [compojure.route :as route])
(:require [imp-rest.settings :as s]))
(defn json-response [data & [status]]
{:status (or status 200)
:headers {"Content-Type" "application/json"}
:body (json/generate-string data)})
(defroutes handler
(GET "/settings" []
(json-response (s/get-settings)))
(GET "/settings/:id" [id]
(json-response (s/get-setting id)))
(PUT "/settings" [id value]
(json-response (s/put-setting id value)))
(route/not-found "Page not found") )
(def app
(-> handler
wrap-json-params))
which exposes this map (of settings):
(ns imp-rest.settings)
(def settings
(atom
{:coordinateName nil
:burnin nil
:nslices nil
:mrsd nil
}))
(defn get-settings []
#settings)
(defn get-setting [id]
(#settings (keyword id)))
(defn put-setting [id value]
(swap! settings assoc (keyword id) value)
value)
and the entry point:
(ns imp-rest.core
(:use ring.adapter.jetty)
(:require [imp-rest.web :as web]))
(defn -main
"Entry point"
[& args]
(do
(run-jetty #'web/app {:port 8080})
);END;do
);END: main
Now when I 'lein run' I can make a (working) request like this:
curl -X PUT -H "Content-Type: application/json" \
-d '{"id" : "coordinateName", "value" : "FOO"}' \
http://localhost:8080/settings
which is what I try to mock with the test. Any help appreciated.
If you want to have :id in your PUT /settings/:id route accepting body in format {"value": "..."}, you need to change your routes definition:
(defroutes handler
(GET "/settings" []
(json-response (s/get-settings)))
(GET "/settings/:id" [id]
(json-response (s/get-setting id)))
(PUT "/settings/:id" [id value]
(json-response (s/put-setting id value)))
(route/not-found "Page not found"))
And change how you call your PUT endpoint in the test:
(w/app
(-> (mock/request
:put
"/settings/coordinateName"
(json/generate-string {:value "FOO"}))
(mock/content-type "application/json")))
What was changed?
:id in your PUT URL route definition (/settings -> /settings/:id)
Your PUT request didn't send a correct request and content type.
If you want to have a PUT /settings route expecting {"id": "...", "value": "..."} request body, then you need to change how you create a mock request:
(w/app
(-> (mock/request
:put
"/settings"
(json/generate-string {:id "coordinateName" :value "FOO"}))
(mock/content-type "application/json"))
Your curl request specifies the parameters as JSON in the body of the PUT request, but your mock request tries to use URL parameters.
There are two options to resolve this:
compojure can automatically translate parameters, but only when the relevant middleware is present -- you have wrap-json-params added to your handler, but you're missing wrap-params. The answer from Piotrek Bzdyl amounts to making these params explicit in the compojure routes.
Alternatively, you can add the ID/value pair as JSON in the body of the mock request using request.mock.body.
Related
I'm trying to adapt the code here https://github.com/danownsthisspace/shorturl/blob/main/src/shorturl/core.clj with this:
(ns todo.core
(:require [clojure.pprint :as pprint]
[muuntaja.core :as m]
[reitit.ring :as ring]
[reitit.ring.middleware.muuntaja :as muuntaja]
[ring.adapter.jetty :as ring-jetty]
[ring.util.response :as r]
[todo.db :as db]))
(defn todo-items-save [req]
(clojure.pprint/pprint req)
(let [title (get-in req [:body-params :title])
content (get-in req [:body-params :content])]
(r/response (str "foooo" title))))
(def app
(ring/ring-handler
(ring/router
[["/"
["" {:handler (fn [req] {:body "hello" :status 200})}]]
["/api/todo" {:post {:handler todo-items-save}
:get (fn [req]
(let [todos db/get-todos]
(r/response todos)))}]
{:data {:muuntaja m/instance :middleware [muuntaja/format-middleware]}}])))
(defn start []
(ring-jetty/run-jetty #'app {:port 3002 :join? false}))
(def server (start))
(.stop server)
but I'm seeing that the body-params values are null. I was wondering why that is when I'm making a post request with the body {"content":"only a test", "title":"second"}. Thank you.
The problem was, that the muuntaja config was passed as part of the
routes instead of argument to ring/router
(def app
(ring/ring-handler
(ring/router
[["/"
["" {:handler (fn [req] {:body "hello" :status 200})}]]
["/api/todo" {:post {:handler todo-items-save}
:get (fn [req]
(let [todos db/get-todos]
(r/response todos)))}]]
; XXX this must be the second argument to `ring/router`
{:data {:muuntaja m/instance
:middleware [muuntaja/format-middleware]}})))
I have a server hosting my API. My API relies on data requested from a third-party API (Spotify). Here are the relevant parts of my API handler:
(ns myapp.api.handler
(:require
[compojure.api.sweet :refer :all]
[ring.util.http-response :refer [ok forbidden no-content not-found bad-request]]
[clj-spotify.core :as spotify]))
(defroutes api-routes
(api
{:middleware [wrap-api]
:swagger {:ui "/api-docs"
:spec "/swagger.json"
:data {:info {:title "My API"
:description "A description for My API"}
:consumes ["application/json"]
:produces ["application/json"]}}}
(context "/api" []
(context "/me" []
(PUT "/player" []
:query-params [device_id :- String]
(handle-player-put device_id))))))
As you'll be able to tell from my route handler, I'd essentially like to forward the response of the third-party API to my API. Here is the handler function, handle-player-put:
(defn handle-player-put [device-id]
(let [available-devices (-> (spotify/get-current-users-available-devices
{}
(lm/oauth-token :spotify))
:devices)]
(doseq [device available-devices]
(when (= (:id device) device-id)
(if (not (:is_restricted device))
(let [response (spotify/transfer-current-users-playback
{:device_ids [device-id]
:play false}
(lm/oauth-token :spotify))]
(case (-> response :error :status)
nil (no-content)
404 (do
(println "Playback response: 404")
(not-found "Spotify could not find the requested resource."))
{:status (-> response :error :status)
:headers {}
:body (-> response :error :message)})))))))
After a successful (spotify/transfer-current-users-playback) request, response binds to {}. An example of a response after an error looks like {:error {:status 502, :message "Bad gateway."}}
No matter whether transfer-current-users-playback is successful or not, I always get a 404 error (with body text Not Found [404]). What am I doing wrong?
doseq always returns nil so your handler returns nil - which is interpreted by compojure as “this handler won’t handle the request; skip to the next handler” and if no other handler handles the request you get a 404 not found.
You should not use (doseq … (when … expr))) if you need to return expr
http://pedestal.io/reference/servlet-interceptor Says this
Before invoking the :enter functions, the servlet interceptor sets up a "terminator" predicate on the context. It terminates the interceptor chain when the context map returned by an interceptor has a response map attached.
My server has this:
(ns wp-server.server
(:gen-class) ; for -main method in uberjar
(:require
[io.pedestal.http :as server]
[io.pedestal.http.route :as route]
[wp-server.service :as service]
[wp-server.datomic :as datomic]))
(defonce runnable-service (atom nil))
(defn -main
"The entry-point for 'lein run'"
[& args]
(println "/nConnecting to datomic...")
(datomic/connect!)
(println "\nCreating your server...")
(reset! runnable-service (server/create-servlet service/service))
(server/start runnable-service))
(defn create-runnable-dev-service
"The entry-point for 'lein run-dev'"
[& args]
(println "\nConnecting to [DEV] database...")
(datomic/connect-dev!)
(println "\nCreating your [DEV] server...")
(-> service/service
(merge {:env :dev
::server/join? false
::server/routes #(route/expand-routes (deref #'service/routes))
::server/allowed-origins {:creds true :allowed-origins (constantly true)}
::server/secure-headers {:content-security-policy-settings {:object-src "none"}}})
server/default-interceptors
server/dev-interceptors
server/create-servlet))
(defn start-dev []
(when-not #runnable-service
(reset! runnable-service (create-runnable-dev-service)))
(server/start #runnable-service))
(defn stop-dev []
(server/stop #runnable-service))
(defn restart-dev []
(stop-dev)
(start-dev))
My service looks like this:
(ns wp-server.service
(:require
[datomic.api :as d]
[io.pedestal.http :as http]
[io.pedestal.http.body-params :as body-params]
[io.pedestal.http.route :as route]
[ring.util.response :as ring-resp]
[wp-server.datomic :as datomic]
[wp-common.client :as wp-common-client]
[wp-common.core :as wp-common-core]
[clojure.spec.alpha :as s]
[ring.util.response :as ring-response]))
(defn about-page
[request]
(ring-resp/response (format "Clojure %s - served from %s"
(clojure-version)
(route/url-for ::about-page))))
(def home-page
{:name :home-page
:enter (fn [context]
(prn "TWO")
(assoc context :response {:status 200 :body "Hello, world!"}))})
(defn db-test-page
[{:keys [:database]}]
(ring-resp/response
(prn-str
(d/q '[:find ?text
:where
[?e :advisor/first-name ?text]]
database))))
(def common-interceptors [datomic/db-interceptor (body-params/body-params) http/html-body])
(defn create-spec-validator
[spec]
{:name :validate-spec
:enter
(fn [{{:keys [:edn-params]} :request :as context}]
(prn "ONE")
(if-let [explained (s/explain-data spec edn-params)]
(assoc context
:response {:status 400 :body explained})))})
(def routes #{
;["/" :get (conj common-interceptors `home-page)]
["/clients" :post (conj common-interceptors (create-spec-validator ::wp-common-client/schema) home-page)]
["/db-test" :get (conj common-interceptors `db-test-page)]
["/about" :get (conj common-interceptors `about-page)]})
(def service {:env :prod
::http/routes routes
::http/type :jetty
::http/port 5000
::http/container-options {:h2c? true
:h2? false
:ssl? false}})
When sending a request to localhost:5000/clients with a body that does not pass spec, the create-spec-validator interceptor adds a response to the context. I have confirmed this by logging the context in the home-page interceptor. I would expect the home-page interceptor to be skipped as per documentation. This does not happen. Instead the :enter function of the home-page interceptor is called and the response overwritten.
Why isn't the home-page interceptor being skipped when the create-spec-validator prior to it is returning context with a response?
If you track down the termination code, it invokes
(defn response?
"True if the supplied value is a valid response map."
{:added "1.1"}
[resp]
(and (map? resp)
(integer? (:status resp))
(map? (:headers resp))))
to test if there is a valid response-map in :response. Try setting an empty map in :headers in your response: it should terminate then.
Ideally, of course, you'll set Content-Type to be something meaningful.
I'm using the below code to try and access some json input in a PUT request however what I get returned has :body {}, I'm not sure what I'm doing wrong?
(ns compliant-rest.handler
(:use compojure.core ring.middleware.json)
(:require [compojure.handler :as handler]
[compojure.route :as route]
[ring.util.response :refer [response]]
[clojure.data.json :refer [json-str]]))
(defroutes app-routes
(PUT "/searches" {body :params} (response body))
(route/resources "/")
(route/not-found "Not Found"))
(def app
(-> (handler/site app-routes)
(wrap-json-body)
(wrap-json-response)))
(app {
:request-method :put
:uri "/searches"
:content-type "application/json"
:body (with-in-str (json-str {:field "value"}))
})
;; {:status 200, :headers {"Content-Type" "application/json; charset=utf-8"}, :body "{}"}
Also, I'm new to Clojure/Lisp, any comments about my syntax and style would be appreciated.
Two things stand out:
The unparsed request body is not supposed to be a string, but an InputStream. This means your test expression won't work as is.
wrap-json-body replaces (:body request) with a clojure data structure. It does not put anything in (:params request) or (:body (:params request)). You want wrap-json-params for that.
Thanks to Joost and the comments I found there is a ring function ring.util.io.string-input-stream that does what I mistakenly thought with-in-str did. Finally I had the following working:
(ns compliant-rest.handler
(:use compojure.core ring.middleware.json)
(:require [compojure.handler :as handler]
[compojure.route :as route]
[ring.util.response :refer [response]]
[ring.util.io :refer [string-input-stream]]
[clojure.data.json :refer [json-str]]))
(defroutes app-routes
(PUT "/searches/:id" {params :params body :body}
(response body))
(route/resources "/")
(route/not-found "Not Found"))
(def app
(-> (handler/site app-routes)
(wrap-json-body)
(wrap-json-response)))
;; Example request
(app {
:request-method :put
:uri "/searches/1"
:content-type "application/json"
:body (string-input-stream (json-str {:key1 "val1"}))
})
;; {:status 200, :headers {"Content-Type" "application/json; charset=utf-8"}, :body "{\"key1\":\"val1\"}"}
It's so awesome that I can just create a simple map and call my api's entry point without needing any sort of server or mocking. I'm totally being pulled into this whole dynamic languages thing with Clojure, the repl and light table!
I'm trying to get started with Clojure and Clojurescript by implementing a simple web app. Things are going pretty good so far and reading from different tutorials I've come up with the code below:
core.clj:
(ns myapp.core
(:require [compojure.core :as compojure]
[compojure.handler :as handler]
[compojure.route :as route]
[myapp.controller :as controller]))
(compojure/defroutes app-routes
(compojure/GET "/" [] controller/index)
(route/resources "/public")
(route/not-found "Not Found"))
(def app
(handler/site app-routes))
controller.clj:
(ns myapp.controller
(:use ring.util.response)
(:require [myapp.models :as model]
[myapp.templates :as template]))
(defn index
"Index page handler"
[req]
(->> (template/home-page (model/get-things)) response))
templates.clj:
(ns myapp.templates
(:use net.cgrand.enlive-html)
(:require [myapp.models :as model]))
(deftemplate home-page "index.html" [things]
[:li] (clone-for [thing things] (do->
(set-attr 'data-id (:id thing))
(content (:name thing)))))
The problem is I can't display non-ascii characters on the page and I don't know how to set HTTP headers on a page.
I see solutions like this but I simply can't figure out where place them in my code:
(defn app [request]
{:status 200
:headers {"Content-Type" "text/plain"}
:body "Hello World"})
P.S: Any suggestions about style and/or code organization are welcome.
Use ring.util.response:
(require '[ring.util.response :as r])
Then on your index function:
(defn index
"Index page handler"
[req]
(-> (r/response (->> (template/home-page (model/get-things)) response))
(r/header "Content-Type" "text/html; charset=utf-8")))
You can chain other actions on the response such as set-cookie and whatnot:
(defn index
"Index page handler"
[req]
(-> (r/response (->> (template/home-page (model/get-things)) response))
(r/header "Content-Type" "text/html; charset=utf-8")
(r/set-cookie "your-cookie-name"
"" {:max-age 1
:path "/"})))