This question already has answers here:
How to run golang tests sequentially?
(5 answers)
Closed 12 months ago.
I am new to Go and want to write a unit test for this small API:
login request.
logout request.
I expect them to be executed by order and both requests are successful.
However, when I execute the TestAPI, the last assert is always wrong and tells me the current user (abc#abc) is not logged in. I know they run in parallel (thus when handling logout request, the backend cookie hasn't stored this username yet) but I don't know how to rewrite so that the login request always happens before the logout request.
I don't want to waste your time, but I did google it for quite a while but found no solution for my case.
Many thanks for your help!
func PostJson(uri string, param map[string]string, router *gin.Engine) *httptest.ResponseRecorder {
jsonByte, _ := json.Marshal(param)
req := httptest.NewRequest("POST", uri, bytes.NewReader(jsonByte))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
return w
}
func TestAPI(t *testing.T) {
server := setUpServer()
var w *httptest.ResponseRecorder
param := make(map[string]string)
param["email"] = "abc#abc"
param["password"] = "123"
param2 := make(map[string]string)
param2["email"] = "abc#abc"
urlLogin := "/login"
w = PostJson(urlLogin, param, server)
assert.Equal(t, 200, w.Code)
assert.Equal(t, w.Body.String(), "{\"msg\":\"login success\",\"status\":\"success\"}")
urlLogout := "/logout"
w = PostJson(urlLogout, param2, server)
assert.Equal(t, 200, w.Code)
assert.Equal(t, w.Body.String(), "{\"msg\":\"logout success\",\"status\":\"success\"}")
Statements within a single test run sequentially
I know they run in parallel
The statements within a single test run sequentially. Those statements are all in the same top level TestAPI test function, so they run sequentially.
On Cookies
I know they run in parallel (thus when handling logout request, the backend cookie hasn't stored this username yet)
Cookies are held by the frontend, and their content can't be trusted
There is no such thing as a "backend" cookie. A cookie is a response header from the backend that the client includes in the request headers of subsequent requests.
The implication of this is that clients have full control over the content of their cookies. Generally cookies contain an unguessable random value to identify the user, which is associated with user data in backend session storage.
If you really are putting the username in the cookie, and then trusting that username, your application's security is trivial to bypass (I just set the name of whatever user I'd like to be in my cookie before making a request).
Gin should supply a secure session management system, maybe backed by something like redis. Make sure you're using it, and not really putting user names in cookies.
You the client must store the cookie and include it in subsequent requests
Your http client bears the responsibility of including cookies from prior responses in subsequent requests. You're doing nothing to include the response cookie from the login, within the request cookie from the logout. Thus the logout request is "not logged in".
What you need is a "cookie jar," a common term for a mechanism to store and include cookies for subsequent requests. One is provided by https://pkg.go.dev/net/http/cookiejar and can be added to an http.Client, but you're not actually using an http.Client - you're calling your server handler on the request directly.
Simple option: let http.Client handle request cookies for you:
Refactor your test to run a "real" http server with testhttp and then make
"real" requests against the server.
Use https://pkg.go.dev/net/http/httptest#Server to start a test http server on localhost, with the handler you provide from you router.
Create an http.Client with a jar (https://pkg.go.dev/net/http#Client.Jar)
Then simply reuse the http client to make the requests, cookies included.
https://pkg.go.dev/net/http/cookiejar#example-New has a good example of how this could be done.
More Work: store cookies in your case, with ResponseRecorder and a direct c all to the http response handler
If you want to do what http.Client could do for you, create a cookie jar at the test level.
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
t.Fail(err)
}
Include the cookies from the jar in every request and return add the cookies from the response to the jar. The obvious place to do this is your PostJson code, to which the jar could be passed as an additional argument.
Before you make the request, get the jar's cookies for the url with https://pkg.go.dev/net/http#CookieJar.Cookies. Then add those cookies to the request by iterating over the list of cookies and calling req's https://pkg.go.dev/net/http#Request.AddCookie for each.
Then "make" the request as you do now.
After the response is returned, you must add the cookies to the cookie jar.
You can access response cookies via https://pkg.go.dev/net/http/httptest#ResponseRecorder.Result on your response recorder, which will return an * http.Response. You can then call https://pkg.go.dev/net/http#Response.Cookies on that response.
Add those cookies to the jar with https://pkg.go.dev/net/http#CookieJar.SetCookies.
The code might look something like:
func PostJson(uri string, param map[string]string, router *gin.Engine, jar http.CookieJar) *httptest.ResponseRecorder {
jsonByte, _ := json.Marshal(param)
req := httptest.NewRequest("POST", uri, bytes.NewReader(jsonByte))
for _, cookie := range jar.Cookies(uri) {
req.AddCookie(cookie)
}
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
jar.SetCookies(uri, w.Result().Cookies())
return w
}
A final note on the value of reproducible code in questions.
If you would have made your code something I could run without significant modification, I would have spent time implementing and testing my suggested changes, and then I could give you a working example. So next time you have a good stack over flow question, take the time to invest in complete code that we can run, maybe in a Go Playground share. In addition to making it a lot easier to provide a high qualityu answer, it's also excellent practice for you!
Related
The setup
I have an identity provider based on IdentityServer4 with AD FS as an external identity provider.
I have 3 different types of clients:
a production hybrid flow client based on OWIN lib
a test hybrid flow client (the latest sample client from the IdentityServer4 repo)
implicit flow clients (Angular)
The problem
The test client and the implicit flow clients work as expected.
When logging out from the OWIN client, the user is logged out from my IdP, but not from AD FS.
Some analysis
When logging out from the OWIN client, cookies are not sent when logging out from AD FS. In consequence, AD FS does not delete any cookies and thus the user stays logged in at the AD FS.
The ultimate reason is that the corresponding request is an xhr request which cannot send cookies (see also SAML logout request is not sending cookies to IdP, although we don't use SAML).
To summarize here are the calls you can observe in the browser:
Request GET https://myidp/connect/endsession
Query Parameters post_logout_redirect_uri=https://xxx, id_token_hint=xxx, x-client-SKU=ID_NET45, x-client-ver=5.5.0.0
Response Redirect to https://myidp/account/logout?logoutId=xxx
Request GET https://myidp/account/logout
Query Parameters logoutId=xxx
Response Redirect to https://myadfs/oauth2/logout?post_logout_redirect_uri=xxx&id_token_hint=xxx&state=xxx&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.5.0.0
Request GET https://myadfs/oauth2/logout
Request type xhr
Query parameters post_logout_redirect_uri=xxx, id_token_hint=xxx, state=xxx, x-client-SKU=ID_NETSTANDARD2_0, x-client-ver=5.5.0.0
Response Ok
For the OWIN client, the last call is of type xhr, for the other clients, it is of type document. I think something in the initial call from the client to myidp must be different such hat the redirect chain ends in a xhr request. What is this something? How can I change this behaviour?
I checked to headers of each call meticulously. Most suspect to me look the Sec-Fetch-Headers (the rest is pretty much equal for all clients), but I don't understand what they actually do.
For the OWIN client:
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
For the other clients:
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
I appreciate any hint!
The post I cited above actually was perfectly right: We have to interrupt the chain of xhr redirects and do a normal browser redirect instead.
However, we needed some time to figure out how to achieve this. Here is our solution:
The Logout action in our AuthorizationController returned Redirect, we had to replace it by a Ok response. This means replace
public class AuthorizationController : Controller
{
public ActionResult Logout()
{
Request.GetOwinContext().Authentication.SignOut("Cookies");
// ...Code for clearing cookies...
return Redirect("/myroute");
}
}
by
public class AuthorizationController : Controller
{
public ActionResult Logout()
{
Request.GetOwinContext().Authentication.SignOut("Cookies");
// ...Code for clearing cookies...
return new HttpStatusCodeResult(HttpStatusCode.OK);
}
}
The function I am trying to test accepts data structs, forms a query_dsl and then makes a /_search call to elastic search with the formed query.
Hence I want to assert on the query_dsl and url which gets formed.
I am using github.com/jarcoal/httpmock to mock net/http requests in my unit tests. As per the doc it exposes func GetCallCountInfo() map[string]int to validate how many times a particular endpoint was hit.
But I am also interested in knowing what was the request body when this call was made.
http.Client is not exposed, hence can not override/mock that for testing.
If it is not possible using this package then is there any other library which can mock the network request and also gives hold of request body?
Following #georgeok Suggestion,
We can create a mock http server and capture the request body when request is made.
Following is the code snippet to create a server and store the request body.
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
actualRequestBody, err = ioutil.ReadAll(req.Body)
check(err)
// Send mock response to be tested
_, err := rw.Write(bytes)
check(err)
}))
defer server.Close()
Now our request body is stored on actualRequestBody variable, and we can assert on this for correctness.
The only thing necessary to make sure this works is to make the call at host server.URL. As it spins up a server on the address mentioned at server.URL. So if the call from your code is being made to different server this will not catch it.
You know, web applications needs sessions or cookies to authentication. I trying to build web application with Vue.JS and Flask microframework for example ERP or CRM.
I'm confused. How can I work with sessions? Let's think we have a code like this in the Flask:
import os
from flask import Flask, request, jsonify, abort, session
app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') or \
'e5ac358c-f0bf-11e5-9e39-d3b532c10a28'
#app.route('/login', methods=['POST'])
def user_login():
user = request.form['user']
session['isLogged'] = True
return jsonify({'status': session['isLogged']})
#app.route('/user-info')
def user_info():
if 'isLogged' in session:
return jsonify({'user': 'ali'})
else:
return jsonify({'error': 'Authentication error'})
and our front-end codes should be like this:
mounted() {
this.checkIsLogged();
},
methods: {
checkIsLogged() {
fetch('http://127.0.0.1:5000/user-info', {
mode: 'no-cors',
method: 'GET',
}).then((resp) => {
return resp;
}).then((obj) => {
if(obj.user) {
this.status = true
}
})
},
login() {
let frmData = new FormData(document.querySelector("#frmLogin"));
fetch('http://127.0.0.1:5000/login', {
mode: 'no-cors',
method: 'POST',
body: frmData,
}).then((resp) => {
return resp;
}).then((obj) => {
this.status = obj.status
})
}
}
Everything is normal until I refresh the page. When I refresh the page, I lose the sessions.
Server-side sessions are important for many reasons. If I use localStore or something like that how could be secure I have no idea.
I need some help who worked on similar projects. You can give me suggestions. Because I never worked similar projects.
Other stuff I've read on this topic:
Single page application with HttpOnly cookie-based authentication and session management
SPA best practices for authentication and session management
I'm still confused to about what can I do.
Session handling is something your SPA doesn't really care much about. The session is between the user-agent (browser) and the server. Your vue application doesn't have much to do with it. That's not to say you can't do something wrong, but usually the issue is not with your front end.
That being said it's tough do give an answer to this question because we don't really know what's wrong. What I can do is give you instructions on how you can diagnose this kind of problem. During this diagnosis you'll figure out where the actual issue is and, at least for me, it usually becomes obvious what I need to do.
Step 1)
Use some low level HTTP tool to check the Server response (personally I use curl or Postman when lazy). Send the login request to the server and take a look at the response headers.
When the login is successful you should have a header "Set-Cookie", usually with a content of a "sessionid" or whatever key you're using for sessions.
If you don't see a "Set-Cookie" one of the following is true:
Your server did not start a session and thus did not send a session cookie to the client
there's a proxy/firewall/anti-ad- or tracking plugin somewhere filtering out Cookies
If you see the Set-Cookie Header continue with Step 2, otherwise check the manual in regards to sessions in your chosen backend technology.
Step 2)
Thankfully most modern browsers have a developer console which allows you to do two things:
1) Check your HTTP request headers, body and response headers and body
2) Take a look at stored cookies
Using the first feature (in Chrome this would be under the "Network" tab in the developer console) diagnose the request and response. To do so you need to have the developer console open while performing the login in your app. Check the response of the login, it should contain the Set-Cookie if the login was successful.
If the cookie is not present your server doesn't send it, probably for security reasons (cross-origin policies).
If it is present, the cookie must now be present in the cookie store. In chrome developer console, go to the "Application" tab, expand Cookies from the left menu and take a look at the hosts for which cookies are present. There should be a cookie present which was set in the step before. If not the browser didn't accept the cookie. This usually happens when your cookie is set for a certain domain or path, which isn't the correct one. In such a case you can try to set the domain and/or path to an empty or the correct value (in case of the path a "/").
If your cookie is present, go to step 3
Step 3)
Remember when I said the app has nothing to do with the session. Every request you send either with ajax or simply entering a valid URL in the browser sends all cookies present for this host in the request headers. That is unless you actively prevent whatever library you're using to do so.
If your request doesn't contain the session cookie one of the following is usually true:
the usage of your http library actively prevents sending of cookies
you're sending a correct request but the cookie-domain/path doesn't match the request host/path and is thus not sent along
your cookie is super shortlived and has already expired
If your cookie is sent correctly then your sessions handling should work unless your server doesn't remember that session or starts a new session regardless of an existing session.
I realise this question is quite old and this extensive answer comes way too late, however someone with similar problems may be able to profit from it.
When I create new request for WKWebView with authentication cookie and send the request, WKWebView correctly loads protected web page:
let req = NSMutableURLRequest(URL: NSURL(string: urlPath)!)
let headers = NSHTTPCookie.requestHeaderFieldsWithCookies([myAuthCookie]);
req.allHTTPHeaderFields = headers;
webView.loadRequest(req)
The problem is, that when user clicks on a any link in the web page, with new request WKWebView loses authentication cookie and is redirected to logon page. Cookie domain and path are filled and correct.
I am aware of the missing functionality of WKWebView mentioned here.
Thanks in advance for any idea.
The best thing to do is to store your cookie into the
[NSHTTPCookieStorage sharedHTTPCookieStorage]
Then each time you want to load the request, call this function instead:
- (void)loadRequest:(NSURLRequest *)request {
if (request.URL) {
NSDictionary *cookies = [NSHTTPCookie requestHeaderFieldsWithCookies:[[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:request.URL]];
if ([cookies objectForKey:#"Cookie"]) {
NSMutableURLRequest *mutableRequest = request.mutableCopy;
[mutableRequest addValue:cookies[#"Cookie"] forHTTPHeaderField:#"Cookie"];
request = mutableRequest;
}
}
[_wkWebView loadRequest:request];
}
It extract the right cookies from shared cookies and includes it into your request
I suppose when you set it in the request you are sending the cookie to the server but NOT setting it in the WKWebview. The cookies are usually set by the server in the "Set-Cookie" header and then it should be persisted. So if if you don't have an issue with cookie passing all the way to the server and back you can do a trick:
send the cookie in the first request
make the server send it back in the "Set-Cookie" header
every subsequent request should have the cookie
I haven't tried the approach yet but will be very surprised if it doesn't work.
The alternative as mentioned by Sebastien could be do inject it via javascript. Be mindful though that you cannot set "HTTP-Only" flag this way and the cookie will be available by all the scripts running (https://www.owasp.org/index.php/HttpOnly).
I'm still trying to find a natural way to set the cookie but I don't think it exists.
Hope it helps.
You can inject some javascript into the we view to load the cookies so that requests initiated by the web view will also have your cookies. See the answer to this question for more details:
https://stackoverflow.com/a/26577303/251687
I have the following NancyFX unit test.
var browser = new Browser(new UnitTestBootstrapper());
var response = browser.Post("/login", with =>
{
with.FormValue("UserName", userName);
with.FormValue("Password", password);
});
response.ShouldHaveRedirectedTo("/home");
You can see that I use an instance of Nancy.Testing.Browser to POST some form values. I would like to capture this Http request in Fiddler but I am not sure how to set-up the Browser (a proxy perhaps?)
Thanks
You can't because they never hit the network; that's the whole point of the browser class - to give you end to end testing without the performance hit/configuration issues of having to use hosting/http/networking/browser rendering.
If you want to go via the networking stack then use something like Selenium, or spin up a self host and poke it with EasyHttp or manually with HttpClient.