Search code examples
javahttpcookiesjuniteasymock

Can I reuse a captured value in a subsequent mock, with EasyMock?


I'm trying to do E2E testing (or as close as I can get to E2E) for a Jetty application. I have a neat set-up involving testcontainers and minimal mocking, and everything works in principle, except that I am now having to mock the HTTP workflow that would be handled by Jetty, because I run my tests with JUnit, and all of this because I need to test methods that require authentication -- and yes, I could mock the authentication layer, but I'd rather not for reasons.

Anyway, this is what I'm ending up doing:

String someSegueAnonymousUserId = "9284723987anonymous83924923";

HttpSession httpSession = createNiceMock(HttpSession.class);
// At first, an anonymous user is "created"
expect(httpSession.getAttribute(Constants.ANONYMOUS_USER)).andReturn(null).atLeastOnce();
expect(httpSession.getId()).andReturn(someSegueAnonymousUserId).atLeastOnce();
replay(httpSession);

Capture<Cookie> capturedCookie = Capture.newInstance(); // new Capture<Cookie>(); seems deprecated

// This is the HTTP request for the login step
HttpServletRequest loginRequest = createNiceMock(HttpServletRequest.class);
expect(loginRequest.getSession()).andReturn(httpSession).atLeastOnce();
replay(loginRequest);

// The login process takes the auth cookie and sticks it into the HTTP response
// I capture the cookie because I'm going to need it for subsequent requests, to prove that I'm logged in
HttpServletResponse loginResponse = createNiceMock(HttpServletResponse.class);
loginResponse.addCookie(and(capture(capturedCookie), isA(Cookie.class)));
expectLastCall().atLeastOnce();
replay(loginResponse);

// This is the request for the endpoint I'm going to test
HttpServletRequest createBookingRequest = createNiceMock(HttpServletRequest.class);
// I expect that the endpoint method will check authentication by grabbing the cookies from the request
// Test case fails here: I can't find a way to cast the Object[] returned by the toArray() method to a Cookie[] which is what getCookies() is expected to return.
expect(createBookingRequest.getCookies()).andReturn((Cookie[]) Collections.singletonList(capturedCookie).toArray()).atLeastOnce();
replay(createBookingRequest);

// OK, so, this logs me in, and it works just fine, the cookie is created, and it is valid as far as I can tell
RegisteredUserDTO testUsers = userAccountManager.authenticateWithCredentials(loginRequest, loginResponse, AuthenticationProvider.SEGUE.toString(), "[email protected]", "testpassword", false);
// I don't even get here at all because of that failure above
Response createBookingResponse = eventsFacade.createBookingForMe(createBookingRequest, "someEventId", null);

Now, either there is a way of fixing this, or I'm doing it terribly wrong and I should be doing things very differently. However, I can't find much guidance on the Internet, so my suspicion that I'm doing something that I'm not supposed to do.

Any pointers to how I should do things differently?


Solution

  • As you indicated in your code comments, the problem seems to be related to the following line:

    expect(createBookingRequest.getCookies()).andReturn((Cookie[]) Collections.singletonList(capturedCookie).toArray()).atLeastOnce();
    

    which is originating a ClassCastException:

    java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljavax.servlet.http.Cookie;
    

    As I indicated in my comment to your question too, please, try using the Capture class getValue() method to obtain a reference to the captured value, and use that value to provide the necessary return information to the createBookingRequest.getCookies() expectation, something like this:

    expect(createBookingRequest.getCookies()).andReturn(new Cookie[] { capturedCookie.getValue() }).atLeastOnce();
    

    Note the use of the getValue() method.

    Be aware that in order to use the proposed solution is very important that you invoke the logingResponse.addCookie(...) method before performing the actual capturedCookie.getValue() invocation.

    I mean, suppose the following code fragment in which I included the proposed change:

    String someSegueAnonymousUserId = "9284723987anonymous83924923";
    
    HttpSession httpSession = createNiceMock(HttpSession.class);
    // At first, an anonymous user is "created"
    expect(httpSession.getAttribute("anonymous")).andReturn(null).atLeastOnce();
    expect(httpSession.getId()).andReturn(someSegueAnonymousUserId).atLeastOnce();
    replay(httpSession);
    
    Capture<Cookie> capturedCookie = Capture.newInstance(); // new Capture<Cookie>(); seems deprecated
    
    // This is the HTTP request for the login step
    HttpServletRequest loginRequest = createNiceMock(HttpServletRequest.class);
    expect(loginRequest.getSession()).andReturn(httpSession).atLeastOnce();
    replay(loginRequest);
    
    // The login process takes the auth cookie and sticks it into the HTTP response
    // I capture the cookie because I'm going to need it for subsequent requests, to prove that I'm logged in
    HttpServletResponse loginResponse = createNiceMock(HttpServletResponse.class);
    loginResponse.addCookie(and(capture(capturedCookie), isA(Cookie.class)));
    expectLastCall().atLeastOnce();
    replay(loginResponse);
    
    // This is the request for the endpoint I'm going to test
    HttpServletRequest createBookingRequest = createNiceMock(HttpServletRequest.class);
    // I expect that the endpoint method will check authentication by grabbing the cookies from the request
    // Test case fails here: I can't find a way to cast the Object[] returned by the toArray() method to a Cookie[] which is what getCookies() is expected to return.
    expect(createBookingRequest.getCookies()).andReturn(new Cookie[] { capturedCookie.getValue() }).atLeastOnce();
    replay(createBookingRequest);
    
    // The rest of your code
    

    If you run the test, it will fail with the following error message:

    java.lang.AssertionError: Nothing captured yet
    
        at org.easymock.Capture.getValue(Capture.java:101)
    

    It makes perfect sense because the captured value will only be available when the "watched" method, the one we want to capture the arguments for, loginResponse.addCookie(...) in this case, is invoked.

    To solve the problem, perhaps you could try reordering your code and only call the capturedCookie.getValue() when the authentication flow is finished, probably in some place in your userAccountManager.authenticateWithCredentials method invocation. For example:

    String someSegueAnonymousUserId = "9284723987anonymous83924923";
    
    HttpSession httpSession = createNiceMock(HttpSession.class);
    // At first, an anonymous user is "created"
    expect(httpSession.getAttribute("anonymous")).andReturn(null).atLeastOnce();
    expect(httpSession.getId()).andReturn(someSegueAnonymousUserId).atLeastOnce();
    replay(httpSession);
    
    Capture<Cookie> capturedCookie = Capture.newInstance(); // new Capture<Cookie>(); seems deprecated
    
    // This is the HTTP request for the login step
    HttpServletRequest loginRequest = createNiceMock(HttpServletRequest.class);
    expect(loginRequest.getSession()).andReturn(httpSession).atLeastOnce();
    replay(loginRequest);
    
    // The login process takes the auth cookie and sticks it into the HTTP response
    // I capture the cookie because I'm going to need it for subsequent requests, to prove that I'm logged in
    HttpServletResponse loginResponse = createNiceMock(HttpServletResponse.class);
    loginResponse.addCookie(and(capture(capturedCookie), isA(Cookie.class)));
    expectLastCall().atLeastOnce();
    replay(loginResponse);
    
    // OK, so, this logs me in, and it works just fine, the cookie is created, and it is valid as far as I can tell
    // Perform the actual authentication flow: I assume it will call under the hood to the loginResponse.addCookie method
    // so everything will be fine when you try getting the captured method in the next step
    RegisteredUserDTO testUsers = userAccountManager.authenticateWithCredentials(loginRequest, loginResponse, AuthenticationProvider.SEGUE.toString(), "[email protected]", "testpassword", false);
    
    // This is the request for the endpoint I'm going to test
    HttpServletRequest createBookingRequest = createNiceMock(HttpServletRequest.class);
    // I expect that the endpoint method will check authentication by grabbing the cookies from the request
    // Test case fails here: I can't find a way to cast the Object[] returned by the toArray() method to a Cookie[] which is what getCookies() is expected to return.
    expect(createBookingRequest.getCookies()).andReturn(new Cookie[] { capturedCookie.getValue() }).atLeastOnce();
    replay(createBookingRequest);
    
    // I don't even get here at all because of that failure above
    Response createBookingResponse = eventsFacade.createBookingForMe(createBookingRequest, "someEventId", null);
    

    With this new setup, be sure that within your userAccountManager authenticateWithCredentials method you invoke loginResponse.addCookie:

    public RegisteredUserDTO authenticateWithCredentials(
      HttpServletRequest loginRequest,
      HttpServletRequest loginResponse, 
      String authType, 
      String username, 
      String password,
      boolean someFlag) {
    
      //...
    
      // Among other things, please, be sure to perform the actual
      // addCookie method invocation
      // Fetchthe cookie as appropriate
      Cookie authCookie = new Cookie("jwt", "124adf45...");
      loginResponse.addCookie(authCookie);
       
      //...
      return testUsers;
    }