package com.vtence.molecule.middlewares;
import com.vtence.molecule.Application;
import com.vtence.molecule.FailureReporter;
import com.vtence.molecule.Request;
import com.vtence.molecule.Response;
import com.vtence.molecule.http.Cookie;
import com.vtence.molecule.lib.CookieJar;
import com.vtence.molecule.session.Session;
import com.vtence.molecule.session.SessionStore;
import org.hamcrest.FeatureMatcher;
import org.jmock.Expectations;
import org.jmock.integration.junit4.JUnitRuleMockery;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import static com.vtence.molecule.testing.CookieJarAssert.assertThat;
import static com.vtence.molecule.testing.RequestAssert.assertThat;
import static com.vtence.molecule.testing.ResponseAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
public class CookieSessionTrackerTest {
@Rule public
JUnitRuleMockery context = new JUnitRuleMockery();
SessionStore store = context.mock(SessionStore.class);
FailureReporter failureReporter = context.mock(FailureReporter.class);
int timeout = (int) TimeUnit.MINUTES.toSeconds(30);
String SESSION_COOKIE = CookieSessionTracker.STANDARD_SERVLET_SESSION_COOKIE;
CookieSessionTracker tracker = new CookieSessionTracker(store).usingCookieName(SESSION_COOKIE);
Request request = new Request();
Response response = new Response();
@Before public void
stubSessionStore() throws Exception {
context.checking(new Expectations() {{
allowing(store).load("existing"); will(returnValue(new Session("existing")));
allowing(store).load("expired"); will(returnValue(null));
}});
}
@Test(expected = IllegalStateException.class) public void
requiresACookieJar() throws Exception {
tracker.handle(request, response);
}
@Test public void
createsSessionsForNewClientsButDoesNotCommitEmptySessions() throws Exception {
CookieJar cookieJar = fillCookieJar();
tracker.connectTo(echoSessionId());
context.checking(new Expectations() {{
never(store).save(with(any(Session.class)));
}});
tracker.handle(request, response);
response.done();
assertNoExecutionError();
assertThat(response).hasBodyText("Session: new");
assertThat(cookieJar).hasNoCookie(SESSION_COOKIE);
}
@Test public void
createsSessionCookieOnceDone() throws Exception {
CookieJar cookieJar = fillCookieJar();
tracker.connectTo(incrementCounter());
context.checking(new Expectations() {{
oneOf(store).save(with(newSession())); will(returnValue("new"));
}});
tracker.handle(request, response);
assertThat(cookieJar).hasNoCookie(SESSION_COOKIE);
response.done();
assertNoExecutionError();
assertThat(cookieJar).hasCookie(SESSION_COOKIE).hasValue("new").isHttpOnly();
}
@Test public void
storesNewSessionIfNotEmpty() throws Exception {
fillCookieJar();
tracker.connectTo(incrementCounter());
context.checking(new Expectations() {{
oneOf(store).save(with(newSession())); will(returnValue("new"));
}});
tracker.handle(request, response);
response.done();
assertNoExecutionError();
assertThat(response).hasBodyText("Counter: 1");
}
@Test public void
tracksExistingSessionsUsingACookieAndSavesSessionIfModified() throws Exception {
fillCookieJar(new Cookie(SESSION_COOKIE, "existing"));
tracker.connectTo(incrementCounter());
Session clientSession = store.load("existing");
clientSession.put("counter", 1);
context.checking(new Expectations() {{
oneOf(store).save(with(sessionWithId("existing"))); will(returnValue("existing"));
}});
tracker.handle(request, response);
response.done();
assertNoExecutionError();
assertThat(response).hasBodyText("Counter: 2");
}
@Test public void
savesExistingSessionEvenIfNotWritten() throws Exception {
fillCookieJar(new Cookie(SESSION_COOKIE, "existing"));
context.checking(new Expectations() {{
oneOf(store).save(with(sessionWithId("existing"))); will(returnValue("existing"));
}});
tracker.handle(request, response);
response.done();
assertNoExecutionError();
}
@Test public void
createsAFreshSessionIfClientSessionHasExpired() throws Exception {
CookieJar cookieJar = fillCookieJar(new Cookie(SESSION_COOKIE, "expired"));
tracker.connectTo(incrementCounter());
context.checking(new Expectations() {{
oneOf(store).save(with(newSession())); will(returnValue("new"));
}});
tracker.handle(request, response);
response.done();
assertNoExecutionError();
assertThat(response).hasBodyText("Counter: 1");
assertThat(cookieJar).hasCookie(SESSION_COOKIE).hasValue("new");
}
@Test public void
doesNotSendTheSameSessionIdIfItDidNotChange() throws Exception {
CookieJar cookieJar = fillCookieJar(new Cookie(SESSION_COOKIE, "existing"));
context.checking(new Expectations() {{
allowing(store).save(with(sessionWithId("existing"))); will(returnValue("existing"));
}});
tracker.handle(request, response);
response.done();
assertNoExecutionError();
assertThat(cookieJar).hasNoCookie(SESSION_COOKIE);
}
@Test public void
destroysInvalidSessions() throws Exception {
CookieJar cookieJar = fillCookieJar(new Cookie(SESSION_COOKIE, "existing"));
tracker.connectTo(writeAndInvalidateSession());
context.checking(new Expectations() {{
oneOf(store).destroy(with("existing"));
}});
tracker.handle(request, response);
response.done();
assertNoExecutionError();
assertThat(cookieJar).hasDiscardedCookie(SESSION_COOKIE);
}
@Test public void
usesPersistentSessionsByDefault() throws Exception {
CookieJar cookieJar = fillCookieJar();
tracker.connectTo(incrementCounter());
context.checking(new Expectations() {{
oneOf(store).save(with(sessionWithMaxAge(-1))); will(returnValue("persistent"));
}});
tracker.handle(request, response);
response.done();
assertNoExecutionError();
assertThat(cookieJar).hasCookie(SESSION_COOKIE).hasMaxAge(-1);
}
@Test public void
setsSessionAndCookieToExpireIfExpirationPeriodSpecified() throws Exception {
CookieJar cookieJar = fillCookieJar();
tracker.expireAfter(timeout);
tracker.connectTo(incrementCounter());
context.checking(new Expectations() {{
oneOf(store).save(with(sessionWithMaxAge(timeout))); will(returnValue("expires"));
}});
tracker.handle(request, response);
response.done();
assertNoExecutionError();
assertThat(cookieJar).hasCookie(SESSION_COOKIE).hasMaxAge(timeout);
}
@Test public void
setsCookieToExpireAfterSessionMaxAge() throws Exception {
CookieJar cookieJar = fillCookieJar();
tracker.connectTo(expireSessionAfter(timeout));
context.checking(new Expectations() {{
allowing(store).save(with(newSession())); will(returnValue("new"));
}});
tracker.handle(request, response);
response.done();
assertNoExecutionError();
assertThat(cookieJar).hasCookie(SESSION_COOKIE).hasMaxAge(timeout);
}
@Test public void
refreshesCookieForExistingSessionsIfMaxAgeSpecified() throws Exception {
CookieJar cookieJar = fillCookieJar(new Cookie(SESSION_COOKIE, "existing"));
tracker.connectTo(expireSessionAfter(timeout));
context.checking(new Expectations() {{
allowing(store).save(with(sessionWithId("existing"))); will(returnValue("existing"));
}});
tracker.handle(request, response);
response.done();
assertNoExecutionError();
assertThat(cookieJar).hasCookie(SESSION_COOKIE).hasMaxAge(timeout);
}
@Test public void
ignoresDroppedSessions() throws Exception {
CookieJar cookieJar = fillCookieJar(new Cookie(SESSION_COOKIE, "existing"));
tracker.connectTo(writeAndDropSession());
context.checking(new Expectations() {{
never(store).save(with(any(Session.class)));
}});
tracker.handle(request, response);
response.done();
assertNoExecutionError();
assertThat(cookieJar).hasNoCookie(SESSION_COOKIE);
}
@Test public void
dropsContentOfCorruptedSessions() throws Exception {
CookieJar cookieJar = fillCookieJar();
tracker.reportFailureTo(failureReporter);
tracker.connectTo(incrementCounter());
Exception saveError = new Exception("Save failed!");
context.checking(new Expectations() {{
oneOf(failureReporter).errorOccurred(saveError);
oneOf(store).save(with(any(Session.class))); will(throwException(saveError));
}});
tracker.handle(request, response);
response.done();
assertNoExecutionError();
assertThat(cookieJar).hasNoCookie(SESSION_COOKIE);
}
@Test public void
createsAFreshSessionIfClientSessionIsCorrupted() throws Exception {
fillCookieJar(new Cookie(SESSION_COOKIE, "corrupted"));
tracker.reportFailureTo(failureReporter);
tracker.connectTo(echoSessionId());
Exception loadError = new Exception("load failed!");
context.checking(new Expectations() {{
oneOf(failureReporter).errorOccurred(loadError);
allowing(store).load(with("corrupted")); will(throwException(loadError));
}});
tracker.handle(request, response);
response.done();
assertNoExecutionError();
assertThat(response).hasBodyText("Session: new");
}
@Test public void
reportsWhenSessionDestructionFails() throws Exception {
fillCookieJar(new Cookie(SESSION_COOKIE, "existing"));
tracker.reportFailureTo(failureReporter);
tracker.connectTo(writeAndInvalidateSession());
Exception destroyError = new Exception("destroy failed!");
context.checking(new Expectations() {{
oneOf(failureReporter).errorOccurred(destroyError);
allowing(store).destroy(with("existing")); will(throwException(destroyError));
}});
tracker.handle(request, response);
response.done();
assertNoExecutionError();
}
@Test public void
usesNewSessionIfRenewed() throws Exception {
CookieJar cookieJar = fillCookieJar(new Cookie(SESSION_COOKIE, "existing"));
tracker.connectTo(writeNewSession());
context.checking(new Expectations() {{
oneOf(store).save(with(newSession())); will(returnValue("new"));
}});
tracker.handle(request, response);
response.done();
assertNoExecutionError();
assertThat(cookieJar).hasCookie(SESSION_COOKIE).hasValue("new");
}
@Test public void
unbindsSessionAfterwards() throws Exception {
fillCookieJar();
tracker.handle(request, response);
assertThat(request).hasAttribute(Session.class, notNullValue());
response.done();
assertNoExecutionError();
assertThat(request).hasNoAttribute(Session.class);
}
@Test public void
unbindsSessionInCaseOfErrorsToo() throws Exception {
fillCookieJar();
tracker.handle(request, response);
response.done(new Exception("Error!"));
assertThat(request).hasNoAttribute(Session.class);
}
private void assertNoExecutionError() throws ExecutionException, InterruptedException {
response.await();
}
private CookieJar fillCookieJar(Cookie... cookies) {
CookieJar cookieJar = new CookieJar(cookies);
cookieJar.bind(request);
return cookieJar;
}
private FeatureMatcher<Session, String> newSession() {
return sessionWithId(null);
}
private FeatureMatcher<Session, String> sessionWithId(String sessionId) {
return new FeatureMatcher<Session, String>(equalTo(sessionId), "session with id", "session id") {
protected String featureValueOf(Session actual) {
return actual.id();
}
};
}
private FeatureMatcher<Session, Integer> sessionWithMaxAge(final int maxAge) {
return new FeatureMatcher<Session, Integer>(equalTo(maxAge), "session with max age", "max age") {
protected Integer featureValueOf(Session actual) {
return actual.maxAge();
}
};
}
private Application echoSessionId() {
return (request, response) -> {
Session session = Session.get(request);
response.body("Session: " + (session.fresh() ? "new" : session.id()));
};
}
private Application incrementCounter() {
return (request, response) -> {
Session session = Session.get(request);
Integer counter = session.contains("counter") ? session.get("counter") : 0;
session.put("counter", counter++);
response.body("Counter: " + counter);
};
}
private Application expireSessionAfter(final int timeout) {
return (request, response) -> {
Session session = Session.get(request);
session.put("written", true);
session.maxAge(timeout);
};
}
private Application writeAndInvalidateSession() {
return (request, response) -> {
Session session = Session.get(request);
session.put("written", true);
session.invalidate();
};
}
private Application writeAndDropSession() {
return (request, response) -> {
Session session = Session.get(request);
session.put("written", true);
Session.unbind(request);
response.done();
};
}
private Application writeNewSession() {
return (request, response) -> {
Session session = new Session();
session.put("written", true);
session.bind(request);
response.done();
};
}
}