package com.vtence.molecule.session; import com.vtence.molecule.support.Delorean; import org.hamcrest.Matcher; import org.jmock.Expectations; import org.jmock.integration.junit4.JUnitRuleMockery; import org.junit.Rule; import org.junit.Test; import java.time.Instant; import java.util.concurrent.TimeUnit; import static com.vtence.molecule.session.SessionMatchers.sameSessionDataAs; import static com.vtence.molecule.session.SessionMatchers.sessionCreatedAt; import static com.vtence.molecule.session.SessionMatchers.sessionUpdatedAt; import static com.vtence.molecule.session.SessionMatchers.sessionWithId; import static java.lang.String.valueOf; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertThat; public class CookieSessionStoreTest { @Rule public JUnitRuleMockery context = new JUnitRuleMockery(); SessionEncoder encoder = context.mock(SessionEncoder.class); Delorean delorean = new Delorean(); Sequence counter = new Sequence(); CookieSessionStore cookies = new CookieSessionStore(counter, encoder).usingClock(delorean); int maxAge = (int) TimeUnit.MINUTES.toSeconds(30); int timeToLive = (int) TimeUnit.DAYS.toSeconds(2); @Test public void encodesSessionDataInCookie() throws Exception { Session data = new Session("42"); Instant now = delorean.freeze(); data.updatedAt(now); data.createdAt(now); data.put("username", "Gorion"); data.put("race", "Human"); data.maxAge(1800); context.checking(new Expectations() {{ allowing(encoder).encode(with(sameSessionDataAs(data))); will(returnValue("<encoded session>")); }}); String cookie = cookies.save(data); assertThat("session cookie", cookie, equalTo("<encoded session>")); } @Test public void generatesIdsForNewSessions() throws Exception { Session data = new Session(); counter.expect(data); context.checking(new Expectations() {{ allowing(encoder).encode(with(sessionWithId(counter.nextId()))); will(returnValue("<...>")); }}); cookies.save(data); } @Test public void decodesSessionFromCookieValue() throws Exception { Session data = new Session() { public String toString() { return "decoded session"; } }; context.checking(new Expectations() {{ allowing(encoder).decode("<encoded session>"); will(returnValue(data)); }}); Session session = cookies.load("<encoded session>"); assertThat("session", session, sameInstance(data)); } @Test public void canRenewExistingSessionsIdsOnSave() throws Exception { Session data = new Session("42"); context.checking(new Expectations() {{ allowing(encoder).encode(with(sessionWithId(counter.nextId()))); will(returnValue("<with renewed id>")); }}); cookies.renewIds(); String renewed = cookies.save(data); assertThat("renewed id", renewed, equalTo("<with renewed id>")); } @Test public void marksSessionUpdateTime() throws Exception { Session data = new Session(); delorean.travelInTime(50); Instant updateTime = delorean.freeze(); context.checking(new Expectations() {{ allowing(encoder).encode(with(sessionUpdatedAt(updateTime))); }}); cookies.save(data); } @Test public void marksSessionCreationTimeForNewSessions() throws Exception { Session data = new Session(); delorean.travelInTime(50); Instant creationTime = delorean.freeze(); context.checking(new Expectations() {{ allowing(encoder).encode(with(sessionCreatedAt(creationTime))); }}); cookies.save(data); } @Test public void discardsExpiredSessions() throws Exception { Session expired = new Session(); expired.maxAge(maxAge); context.checking(new Expectations() {{ allowing(encoder).decode("expired"); will(returnValue(expired)); }}); delorean.travelInTime(timeJump(maxAge)); assertThat("expired session", cookies.load("expired"), nullValue()); } @Test public void discardsStaleSessions() throws Exception { cookies.idleTimeout(maxAge); Session staleSession = new Session(); context.checking(new Expectations() {{ allowing(encoder).decode("stale"); will(returnValue(staleSession)); }}); delorean.travelInTime(timeJump(maxAge)); assertThat("stale session", cookies.load("stale"), nullValue()); } @Test public void limitsSessionsLifetime() throws Exception { cookies.timeToLive(timeToLive); Session dead = new Session(); context.checking(new Expectations() {{ allowing(encoder).decode("dead"); will(returnValue(dead)); }}); delorean.travelInTime(timeJump(timeToLive)); assertThat("dead session", cookies.load("dead"), nullValue()); } @Test public void ignoresCorruptedSessions() throws Exception { context.checking(new Expectations() {{ allowing(encoder).decode("corrupted"); will(returnValue(null)); }}); assertThat("corrupted session", cookies.load("corrupted"), nullValue()); } private long timeJump(int seconds) { return TimeUnit.SECONDS.toMillis(seconds); } private class Sequence implements SessionIdentifierPolicy { private int nextId; private Matcher<Session> session = notNullValue(Session.class); private Sequence() { this(1); } public Sequence(int seed) { this.nextId = seed; } public void expect(Session session) { expect(equalTo(session)); } public void expect(Matcher<Session> matching) { this.session = matching; } public String generateId(Session data) { assertThat("session data", data, session); return valueOf(nextId++); } public String nextId() { return valueOf(nextId); } } }