1

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(), "test-account@test.com", "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?

Morpheu5
  • 2,610
  • 6
  • 39
  • 72
  • 1
    If the problem has to do only with the following line: `expect(createBookingRequest.getCookies()).andReturn((Cookie[]) Collections.singletonList(capturedCookie).toArray()).atLeastOnce();`, have you tried to access the captured value? I mean, using something like this: `expect(createBookingRequest.getCookies()).andReturn(new Cookie[] { capturedCookie.getValue() }).atLeastOnce();`. Note the use of the `Capture` class [`getValue()`method](https://easymock.org/api/org/easymock/Capture.html#getValue--). I usually use other mocking frameworks but I think the rest of your code looks fine. – jccampanero Jun 24 '22 at 22:10
  • Is there any specific reason why you do not simply use `.toArray(new Cookie[0])`? Please letz me know if this helps, then I can convert the comment into an answer, if that detail is really your problem. – kriegaex Jun 26 '22 at 11:34
  • @kriegaex why would I go through the trouble of capturing the cookie and turning it into a singleton collection with the intended purpose of reusing the cookie in the next request if I then instruct the next request to return an empty array of cookies? – Morpheu5 Jun 26 '22 at 12:04
  • 1
    @Morpheu5, maybe you want to read a [tutorial](https://www.baeldung.com/java-collection-toarray-methods) or some [JRE javadoc](https://docs.oracle.com/javase/8/docs/api/java/util/Collection.html#toArray-T:A-): _"Parameters: a - the array into which the elements of this collection are to be stored, if it is big enough; otherwise, a new array of the same runtime type is allocated for this purpose."_ Using a zero-sized array of the desired type as a parameter is kind of a Java idiom. – kriegaex Jun 26 '22 at 13:56

1 Answers1

0

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 gettting the captured method in the next step
RegisteredUserDTO testUsers = userAccountManager.authenticateWithCredentials(loginRequest, loginResponse, AuthenticationProvider.SEGUE.toString(), "test-account@test.com", "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;
}
jccampanero
  • 50,989
  • 3
  • 20
  • 49
  • 1
    OK, so, the issue with the `Cookie[]` is solved by that suggestion and I'm bashing myself for not actually thinking it :) The bigger point though was that I was doing things out of order, so your suggestion to reorder the steps actually allowed me to progress to the next error (which is outside the scope of this question and looks like something I should be able to deal with myself anyway) so thank you! I guess I misunderstood how setting up the expectations worked and assumed that I was just setting things up and they'd be triggered once I call each actual method. So, back to the books ;) – Morpheu5 Jun 26 '22 at 11:59
  • Thank you very much for the feedback @Morpheu5. It is nice to hear that the proposed solution in my original comment and exposed here again, and the proposed reordering suggestion, were helpful. What problem are you facing now? As I explained, I tried reproducing your test, of course, without the actual auth stuff, to a certain extend, and it worked properly. Please, could you elaborate? I will be glad to help if I can. – jccampanero Jun 26 '22 at 14:16
  • oh, I'm facing issues related to the codebase, as in, I haven't configured the various classes well enough, but I just managed to produce a passing test case after excruciatingly configuring everything, and I'm happy with this. The real issue here is that the whole application is configured using Guice, and I'm essentially doing manually what Guice does through "magic". I'd LOVE if I could just reuse the Guice configuration but I'm not sure how to make it fit within JUnit. Any pointers would be much appreciated. – Morpheu5 Jun 27 '22 at 14:20
  • 1
    I am happy to hear that at least you were able to produce a passing test. I haven't used Guice in this way, but I have searched the web and I found these links: [1](https://gist.github.com/virasak/3798194), [2](https://carlosbecker.com/posts/gunit-guice-and-junit-fall-in-love/), [3](https://github.com/marcolamberto/guice-junit-runner), [4](https://stackoverflow.com/questions/5633915/guice-injector-in-junit-tests). Please, consider review them in the provided order, especially the first one. I hope they be of help. Please, do not hesitate to contact me again if you need to, I'll be glad to help – jccampanero Jun 27 '22 at 21:02
  • I still think, like I wrote above under the question, that you could simply write `Collections.singletonList(capturedCookie).toArray(new Cookie[0])` and be done with. Did anyone even give it a try? Why make matters more complicated than necessary? – kriegaex Jul 05 '22 at 10:17
  • Thank you for the feedback @kriegaex. Yes, I agree with you, certainly you can use `Collections.singletonList(capturedCookie.getValue()).toArray(new Cookie[0])` instead of `new Cookie[] { capturedCookie.getValue() }`, but I think the most important point in the problem is that the code should be reordered to make this single line of code works; on the contrary, no matter what we use, the test with actually fail. That is what I tried to explain in the answer. – jccampanero Jul 05 '22 at 22:04