Clojure GET request of ShipStation using clj-http.client - clojure

I'm trying to use a GET request using clj-http.client from ShipStation and getting a 401 error,
they have lots of examples of different languages of how to do this. Javascript seems easy:
var myHeaders = new Headers();
myHeaders.append("Host", "ssapi.shipstation.com");
myHeaders.append("Authorization", "username:password");
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
fetch("https://ssapi.shipstation.com/orders/orderId", requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => console.log('error', error));
Here is my clojure client/get code which works fine without headers and using {:async? true} from other websites such as jsonplaceholder:
(client/get "https://ssapi.shipstation.com/shipments?shipDateStart=2023-01-05&shipDateEnd=2023-01-05"
{:header { "Host" "ssapi.shipstation.com" "Authorization" "username:password"}}
(fn [response] (println "hello" (get response :body)))
(fn [exception] (println "exception message is: " (.getMessage exception))))
can someone help me with the format of this GET request?

Found the answer to calling shipstation api in clj-http:
https://stackoverflow.com/a/25794180/21046503
(clj-http.client/get "http://my.domain.com"
{:basic-auth [username password]})

As per the docs it looks like you want:
(client/get "https://ssapi.shipstation.com/shipments"
{:async? true
:basic-auth "username:password"
:query-params {:shipDateStart "2023-01-05" :shipDateEnd "2023-01-05"
:as :json}
(fn [response] (println "hello" (get response :body)))
(fn [exception] (println "exception message is: " (.getMessage exception))))
That presumes that Basic-Auth is in play which is what the endpoint and api docs suggest but I'm not a shistation user

Related

Clojure / Ring: Response map is nil when adding session key

I want to update a session key on /callback upon logging in. Here's the handler:
(defn callback [{session :session :as request}]
(let [id (:id (get-current-users-profile))
updated-session (assoc session :identity id)]
{:status 200
:headers {"Content-Type" "text/html"}
:body (loading-page)
:session updated-session}))
As long as the session key-value pair is present in the response map, I get a 500 "Response map is nil" response.
Is there something I'm doing wrongly here?
By the way, I can still update the session map (with no errors) on my /logout route by doing the following:
(defn logout [{session :session}]
(-> (resp/redirect "/")
(assoc :session {})))
And here's what calls both callback and logout:
(defroutes main-routes
(GET "/" [] index)
(GET "/login" [] login)
(GET "/callback" [] callback)
(GET "/logout" [] logout))

Compojure-api not respecting body from ring.mock.requests

I'm trying to use the ring.mock.requests library to test an http service. I have this bit of code
(auth-routes
(->
(mock/request :post "/auth/user")
(mock/body {:username "user" :password "pass"})
(mock/content-type "application/json")))
The inner part with the thread-first macro seems to work correctly, but it fails when I try to make the mock request to auth-routes
And this is my route:
(def auth-routes
(context "/auth" []
:tags ["Authentication"]
(POST "/user" []
:return s/Uuid
:body [request m/User]
:summary "Create new user"
(ok (auth/create-user request)))) ...)
This is my schema:
(def User {:username s/Str
:password s/Str})
The exception I'm seeing is
clojure.lang.ExceptionInfo
Request validation failed
{:error (not (map? nil)), :type :compojure.api.exception/request-validation}
It looks like my route is getting nil as the body, and I expect {:username "user" :password "pass"} to be there.
What am I doing wrong and how do I pass a body to this route?
I think you should serialize your data as a json string in your test. Using cheshire:
(cheshire.core/generate-string {:username "user" :password "pass"})
Take a look here or here.

Request map passed to my anonymous function inside a Compojure handler

I'm two days into learning Clojure by writing a simple REST server using ring-clojure and Compojure with ring-json's wrap-json-body middleware.
So far, I have:
A vector users containing the users (with a couple of default users):
(def users [{:id 0 :username "aname"}
{:id 1 :username "anothername"}])
A function (form?) save-user that accepts a map (user) and looks for existing users with the same username. If the username is available, I overwrite the users vector to include the new user before returning HTTP 201. If the username is taken, I simply return HTTP 400:
(defn save-user [user]
(prn users)
(if
(not-any? #(= (:username %) (:username user)) users)
(fn [request]
(def users (conj users user))
(status
(response (str "Saved user with username: " (:username user)))
201))
(status
(response (str "User with username '" (:username user) "' already exists"))
400)))
A route for POST /users which calls save-user with the received map:
(defroutes app-routes
(POST "/users" request (save-user (:body request))))
I don't think it matters, but the middleware is applied like this:
(def app
(-> app-routes
(wrap-cors :access-control-allow-origin "*" :access-control-allow-methods "*")
(wrap-json-response)
(wrap-keyword-params)
(wrap-json-body {:keywords? true :bigdecimals? true})
(wrap-defaults (assoc site-defaults :security false))))
My problem:
For whatever reason, the entire request map is passed to the function i pass as then inside the if. Printing it:
(if
(not-any? #(= (:username %) (:username user)) users)
(fn [request]
(prn request))
...
... gives me this:
{:ssl-client-cert nil, :cookies {}, :remote-addr "0:0:0:0:0:0:0:1", :params {}, :flash nil, :route-params {}, :headers {"host" "localhost:3000", "accept" "*/*", "content-length" "42", "content-type" "application/json", "user-agent" "curl/7.43.0"}, :server-port 3000, :content-length 42, :form-params {}, :compojure/route [:post "/users"], :session/key nil, :query-params {}, :content-type "application/json", :character-encoding nil, :uri "/users", :server-name "localhost", :query-string nil, :body {:username "testusername", :password "testpassword"}, :multipart-params {}, :scheme :http, :request-method :post, :session {}}
The same happens if I pass an anonymous function as the if's else. However, nothing wrong happens when I only pass (status ...), like in the code above.
From what I understand, the request map shouldn't be available inside save-user at all, since it's not passed as an argument. Why is it passed to my anonymous function, and is there any way to simply ignore it?
In save-user you are returning a function that takes a request and returns a response (a handler).
I suppose you should instead just return the response directly. Just replace (fn [request] with (do to wrap multiple expressions in a single form.
(def users (atom [{:id 0 :username "aname"}
{:id 1 :username "anothername"}]))
(defn save-user [user]
(if (not-any? #(= (:username %) (:username user)) #users)
(do
(swap! users conj user)
(status
(response (str "Saved user with username: " (:username user)))
201))
(status
(response (str "User with username '" (:username user) "' already exists"))
400)))
I’ve also changed the global users var to be an atom. Redefining global vars from inside a function is a big no-no in Clojure.

where does ring get the wrap-params information for forms?

I'm trying to delve deeper into ring and have the following code to look at what is going on in the request map:
(require '[ring.adapter.jetty :as serv])
(require '[ring.middleware.params :as wp])
(defn base-handler [params]
{:code 200
:type "txt/html"
:body (str params)})
(defn wp-handler [params]
((wp/wrap-params base-handler) params))
(serv/run-jetty #'wp-handler {:port 8099})
When I want to look at the get query data on the request map, it is there. Say i typed in http://localhost:8099/api/tasks/remove?hello=3 in the browser the response looks something like this:
{:ssl-client-cert nil,
:remote-addr "127.0.0.1",
:scheme :http,
:request-method :get,
:uri "/api/tasks/remove"
:query-string "hello=3"
.... more ....}
and you can see that "hello=3" is in the query string.
But for post, the response has :content-type "application/x-www-form-urlencoded", the entire response looks like this example below and I do not see any form parameters:
{:ssl-client-cert nil, :remote-addr "127.0.0.1", :scheme :http, :request-method :post, :query-string nil, :content-type "application/x-www-form-urlencoded", :uri "/api/tasks/remove", :server-name "localhost", :headers {"user-agent" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.47 Safari/536.11", "origin" "http://localhost:8099", "accept-charset" "ISO-8859-1,utf-8;q=0.7,*;q=0.3", "accept" "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "host" "localhost:8099", "referer" "http://localhost:8099/form.html", "content-type" "application/x-www-form-urlencoded", "cache-control" "max-age=0", "accept-encoding" "gzip,deflate,sdch", "content-length" "23", "accept-language" "en-US,en;q=0.8", "connection" "keep-alive"}, :content-length 23, :server-port 8099, :character-encoding nil, :body #}
My question is in two parts:
What exactly is ring or jetty doing when a form is submitted?
If i'm trying to write my own handler for handling form data, what is a simplified code skeleton of how I am able to access the form query parameters?
I've isolated the 'magic' to this function in params.clj:
(defn- assoc-form-params
"Parse and assoc parameters from the request body with the request."
[request encoding]
(merge-with merge request
(if-let [body (and (urlencoded-form? request) (:body request))]
(let [params (parse-params (slurp body :encoding encoding) encoding)]
{:form-params params, :params params})
{:form-params {}, :params {}})))
in particular this line:
(parse-params (slurp body :encoding encoding) encoding)
but am not sure what it is doing.
Judging by source code of urlencode-form? parse-params is never called as your request is application/x-www-form-urlencoded. Please see description of request types here.
How do you send post request? As far as I understand what happens is parameters are sent in multipart/form-data mode, but request itself is marked as application/x-www-form-urlencoded.

Compojure Routes losing params info

My code:
(defn json-response [data & [status]]
{:status (or status 200)
:headers {"Content-Type" "application/json"}
:body (json/generate-string data)})
(defroutes checkin-app-handler
(GET "/:code" [code & more] (json-response {"code" code "params" more})))
When I load the file to the repl and run this command, the params seems to be blank:
$ (checkin-app-handler {:server-port 8080 :server-name "127.0.0.1" :remote-addr "127.0.0.1" :uri "/123" :query-string "foo=1&bar=2" :scheme :http :headers {} :request-method :get})
> {:status 200, :headers {"Content-Type" "application/json"}, :body "{\"code\":\"123\",\"params\":{}}"}
What am I doing wrong? I need to get at the query-string, but the params map is always empty..
In order to get the query string parsed into the params map, you need to use the params middleware:
(ns n
(:require [ring.middleware.params :as rmp]))
(defroutes checkin-app-routes
(GET "" [] ...))
(def checkin-app-handler
(-> #'checkin-app-routes
rmp/wrap-params
; .. other middlewares
))
Note, that the usage of the var (#'checkin-app-routes) is not strictly necessary, but it makes the routes closure, wrapped inside the middlewares, pick up the changes, when you redefine the routes.
IOW you can also write
(def checkin-app-handler
(-> checkin-app-routes
rmp/wrap-params
; .. other middlewares
))
but then, you need to redefine the handler too, when interactively redefining the routes.