17

EDIT: I'm using Play! version 1.2 (production release)

I want to test controller actions that are secured by Secure module class, so I need to log in prior to testing my controller (otherwise I will be redirected to the login page).

I've tried to log in prior to calling a secured action. Here's what my FunctionalTest looks like:

@Test
public void someTestOfASecuredAction() {
    Map<String, String> loginUserParams = new HashMap<String, String>(); 
    loginUserParams.put("username", "admin"); 
    loginUserParams.put("password", "admin");

    // Login here so the following request will be authenticated:
    Response response = POST("/login", loginUserParams);

    // The following is an action that requires an authenticated user:
    Map<String, String> params;
    params.put("someparam", "somevalue");
    response = POST("/some/secured/action", params);

    assertIsOk(response); // this always fails because it is a 302 redirecting to /login
}

Stepping through the code, I've verified that the login post works - it causes a redirect response with location set to the home page (which indicates a successful login).

But then in the subsequent call to a secured action, I am always redirected to the "/login" page - indicating that my previous login did not stick for the second POST request.

Looking into the source code of FunctionalTest I saw there was an @Before interceptor that clears all cookies. I tried overriding this intercepter in my own intermediary superclass (to preserve the cookies), but that didn't work either.

EDIT: I was confusing the play.mvc.Before interceptor with org.junit.Before - the former for use with Play! controllers, the latter for JUnit tests. The @Before in the FuncitonTest is a JUnit interceptor, so it should have any affect on cookies (since it gets run once prior to the test being run).

I do not want to have to write a Selenium test for every secured action - since almost all will be secured. Is there a way to "fool" the Secure module into believing you're authenticated? Or maybe some other very obvious way for handling this (seemingly common) scenario in a FunctionalTest?

Thanks in advance,

Mark

EDIT: Working code, Codemwnci's answer marked as correct

Codemwnci's answer is correct. Here is my workaround for preserving the cookies from one request to the next:

@Test
public void someTestOfASecuredAction() {
    Map<String, String> loginUserParams = new HashMap<String, String>();
    loginUserParams.put("username", "admin");
    loginUserParams.put("password", "admin");
    Response loginResponse = POST("/login", loginUserParams);

    Request request = newRequest();
    request.cookies = loginResponse.cookies; // this makes the request authenticated
    request.url = "/some/secured/action";
    request.method = "POST";
    request.params.put("someparam", "somevalue");
    Response response = makeRequest(request);
    assertIsOk(response); // Passes!
}
SBerg413
  • 14,515
  • 6
  • 62
  • 88
Mark S
  • 1,488
  • 12
  • 19
  • See http://play.lighthouseapp.com/projects/57987-play-framework/tickets/775 – Havoc P Apr 25 '11 at 18:20
  • For 2.0, in Java, it seems one cannot easily access the cookies (or the session). But instead, one can preserve the HTTP `Set-Cookie` header from the response, and pass that very same header as `Cookie` in subsequent requests. Easy does it. See [Keep session in subsequent Java calls to Play 2.0's fakeRequest](http://stackoverflow.com/questions/9875572/keep-session-in-subsequent-java-calls-to-play-2-0s-fakerequest/). – Arjan Mar 28 '12 at 19:40

4 Answers4

7

I think there must be a misunderstanding of what the @Before interceptor does. It executes before your test is executed. If your test then logs you in, then this event happens AFTER the @Before code has been executed and the results of the secure module should be saved to the Cookie.

Therefore, I can only assume that the Cookie is not being sent with the following request, therefore, I would suggest trying the following...

get the cookie used by the secure cookie from the Response object immediately following your login. Create a Request object and set the Cookie to the request object, then call your POST method, passing in your request object.

I have not tested this code, so not sure how it is going to react to mixing a pre-built request object, and passing in a URL, but not sure what else to suggest.

Codemwnci
  • 54,176
  • 10
  • 96
  • 129
  • I think you are right. I've been able to get a GET request to reuse the authenticated session by putting the first Request's Response cookies into the subsequent Request's cookies. However, I'm still having troubles with a POST (following the authentication request). I'm sure this is my error. Anyway, I'll mark your answer as correct after I've verified being able to get a successful POST request to work too. Thanks! – Mark S Apr 23 '11 at 19:35
  • But there's code in FunctionalTest.java that looks like it's supposed to do this for you. Look in makeRequest() which stores the cookies from the response in savedCookies, and then in POST etc. it stores savedCookies in the new request. The savedCookies gets cleared between tests (but not between requests) by the @Before handler. For some reason though it just doesn't work. I think it's a bug. – Havoc P Apr 23 '11 at 19:47
  • Yeah, I have been looking at that code for 24 hours now, and I though that's what it was supposed to do too. I do have a workaround that is working for a GET request, but not for a POST (yet). Once I figure out a workaround for a POST, I'll post it. You're answer may be right, however, since this does look like a bug in the FunctionalTest. – Mark S Apr 23 '11 at 20:24
  • I'm marking Codemwnci's answer as correct since the workaround was to copy the cookies from the login request's Response to the following Request. However, this seems like a bug in the Play! framework's FunctionalTest class in that the cookies are not being preserved even though code is in the class that is supposed to do just that. I've put my workaround code in the question as an edit – Mark S Apr 24 '11 at 16:50
4

I had the same problem, but in tests with Play 2.0.4.

I solved the problem by following Seb and Codemwnci answers and I built, checking API, the following solution:

@Test
public void listSomething() {
    running(fakeApplication(inMemoryDatabase()), new Runnable() {
        @Override
        public void run() {
            // LOGIN
            final Map<String, String> data = new HashMap<String, String>();
            data.put("email", "user@domain.com");
            data.put("password", "userpassword");

            Result result = callAction(
            controllers.routes.ref.Application.authenticate(),
            fakeRequest().withFormUrlEncodedBody(data));

            // RECOVER COOKIE FROM LOGIN RESULT
            final Cookie playSession = play.test.Helpers.cookie("PLAY_SESSION",
                                                                result);

            // LIST SOMETHING (using cookie of the login result)
            result = callAction(controllers.routes.ref.Application.list(), 
                                fakeRequest().withCookies(playSession));

            /* 
             * WAS RECEIVING 'SEE_OTHER' (303) 
             * BEFORE RECOVERING PLAY_SESSION COOKIE (BECAUSE NOT LOGGED IN).
             *
             * NOW, EXPECTED 'OK'
             */ 
            assertThat(status(result)).isEqualTo(OK); 
            assertThat(contentAsString(result)).contains(
                    "Something found");
        }
    });
}

The Application.list() is something like this:

@Security.Authenticated(Secured.class)
public static Result list() {
   return ok(list.render(...));
}
Community
  • 1
  • 1
Roberto
  • 500
  • 5
  • 9
0

Maybe you are seeing https://bugs.launchpad.net/play/+bug/497408

I think it is supposed to work.

Update: I did some debugging. I think there are two bugs.

  1. In FunctionalTest, cookies are just discarded if they have no maxAge.
  2. In Scope.java, in Session, the "TS" field is only set if the cookie previously existed. Therefore, the first time the session cookie is set, it gets ignored when it's sent back to the server. If you make 3 requests, it seems to work OK between requests 2 and 3, but not between requests 1 and 2, because on request 2 the cookie sent doesn't have a timestamp.

So bug 1 breaks it if you don't set maxAge, and bug 2 breaks it if you do set maxAge.

Update 2: I made some patches that fix it for me: http://play.lighthouseapp.com/projects/57987-play-framework/tickets/775

Havoc P
  • 8,365
  • 1
  • 31
  • 46
  • I don't think so, since I'm using version 1.2 (will add as an edit to question). At first it seemed likely, but I've been able to get one test to pass by micro-managing cookies - which indicates the session is being used in multiple requests. – Mark S Apr 23 '11 at 19:30
  • If you read the last comment on the bug, even though it was supposedly closed, it still seems to be there. I'm using play 1.2 also and I get a new session on every request in my FunctionalTest, so I have the same problem you do. If you look at the code for FunctionalTest there's this savedCookies variable that looks like it's supposed to keep the cookies across requests, but for me it isn't working. I have been too lazy so far to debug it but I think it's a FunctionalTest bug. i.e. the launchpad bug needs to be reopened, basically. – Havoc P Apr 23 '11 at 19:42
  • One theory, it looks like I have commented out in my application.conf: # application.session.maxAge=1h ; it looks like this will cause the session cookie to not set max age; and then in FunctionalTest.java: if(e.getValue().maxAge != null && e.getValue().maxAge > 0) { savedCookies.put(e.getKey(), e.getValue()); } So to test this theory you'd uncomment maxAge config option and see if it helps. – Havoc P Apr 23 '11 at 19:55
  • No, that didn't help (uncommenting the # application.session.maxAge=1h line). Thanks for the idea, though. – Mark S Apr 23 '11 at 20:23
  • oh well. be sure to post an update if you figure it out, I would like to know the answer too... – Havoc P Apr 24 '11 at 01:18
  • I tried debugging it and updated the answer with what I think is wrong. – Havoc P Apr 25 '11 at 15:11
0

Want to point out that there is an error in Mark S' final solution.

This is wrong: request.url = "/some/secured/action";

This is right: request.path = "/some/secured/action";

"url" should be a full url.

Dan
  • 10,531
  • 2
  • 36
  • 55
DarkNeuron
  • 7,981
  • 2
  • 43
  • 48