/**
* Copyright (C) 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ninja.session;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.Collection;
import ninja.Context;
import ninja.Cookie;
import ninja.Result;
import ninja.utils.Clock;
import ninja.utils.CookieEncryption;
import ninja.utils.Crypto;
import ninja.utils.NinjaConstant;
import ninja.utils.NinjaProperties;
import ninja.utils.SecretGenerator;
import org.hamcrest.CoreMatchers;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@RunWith(Parameterized.class)
public class SessionImplTest {
@Mock
private Context context;
@Mock
private Result result;
@Captor
private ArgumentCaptor<Cookie> cookieCaptor;
private Crypto crypto;
private CookieEncryption encryption;
@Mock
NinjaProperties ninjaProperties;
@Mock
Clock clock;
@Parameter
public boolean encrypted;
/**
* This method provides parameters for {@code encrypted} field. The first set contains {@code false} so that
* {@link CookieEncryption} is not initialized and test class is run without session cookie encryption. Second set
* contains {@code true} so that sessions cookies are encrypted.
*
* @return
*/
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] { { false }, { true } });
}
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(
ninjaProperties
.getInteger(NinjaConstant.sessionExpireTimeInSeconds))
.thenReturn(10000);
when(
ninjaProperties.getBooleanWithDefault(
NinjaConstant.sessionSendOnlyIfChanged, true))
.thenReturn(true);
when(
ninjaProperties.getBooleanWithDefault(
NinjaConstant.sessionTransferredOverHttpsOnly, true))
.thenReturn(true);
when(
ninjaProperties.getBooleanWithDefault(
NinjaConstant.sessionHttpOnly, true)).thenReturn(true);
when(ninjaProperties.getOrDie(NinjaConstant.applicationSecret))
.thenReturn(SecretGenerator.generateSecret());
when(ninjaProperties.getOrDie(NinjaConstant.applicationCookiePrefix))
.thenReturn("NINJA");
when(clock.currentTimeMillis())
.thenReturn(System.currentTimeMillis());
when(ninjaProperties.getBooleanWithDefault(NinjaConstant.applicationCookieEncrypted, false)).thenReturn(encrypted);
encryption = new CookieEncryption(ninjaProperties);
crypto = new Crypto(ninjaProperties);
}
@Test
public void testSessionDoesNotGetWrittenToResponseWhenEmptyAndOnlySentWhenChanged() {
Session sessionCookie =
createNewSession();
sessionCookie.init(context);
// put nothing => empty session will not be sent as we send only changed
// stuff...
sessionCookie.save(context);
// no cookie should be set as the flash scope is empty...:
verify(context, never()).addCookie(Matchers.any(Cookie.class));
}
@Test
public void testSessionCookieSettingWorks() throws Exception {
Session sessionCookie = createNewSession();
sessionCookie.init(context);
sessionCookie.put("hello", "session!");
// put nothing => intentionally to check if no session cookie will be
// saved
sessionCookie.save(context);
// a cookie will be set
verify(context).addCookie(cookieCaptor.capture());
// verify some stuff on the set cookie
assertEquals("NINJA_SESSION", cookieCaptor.getValue().getName());
// assert some stuff...
// Make sure that sign is valid:
String cookieString = cookieCaptor.getValue().getValue();
String cookieFromSign = cookieString.substring(cookieString.indexOf("-") + 1);
String computedSign = crypto.signHmacSha1(cookieFromSign);
assertEquals(computedSign, cookieString.substring(0, cookieString.indexOf("-")));
if (encrypted) {
cookieFromSign = encryption.decrypt(cookieFromSign);
}
// Make sure that cookie contains timestamp
assertTrue(cookieFromSign.contains(Session.TIMESTAMP_KEY));
}
@Test
public void testHttpsOnlyWorks() throws Exception {
Session sessionCookie = createNewSession();
sessionCookie.init(context);
sessionCookie.put("hello", "session!");
// put nothing => intentionally to check if no session cookie will be
// saved
sessionCookie.save(context);
// a cookie will be set
verify(context).addCookie(cookieCaptor.capture());
// verify some stuff on the set cookie
assertEquals(true, cookieCaptor.getValue().isSecure());
}
@Test
public void testNoHttpsOnlyWorks() throws Exception {
// setup this testmethod
when(
ninjaProperties.getBooleanWithDefault(
NinjaConstant.sessionTransferredOverHttpsOnly, true))
.thenReturn(false);
Session sessionCookie = createNewSession();
sessionCookie.init(context);
sessionCookie.put("hello", "session!");
// put nothing => intentionally to check if no session cookie will be
// saved
sessionCookie.save(context);
// a cookie will be set
verify(context).addCookie(cookieCaptor.capture());
// verify some stuff on the set cookie
assertEquals(false, cookieCaptor.getValue().isSecure());
}
@Test
public void testHttpOnlyWorks() throws Exception {
Session sessionCookie = createNewSession();
sessionCookie.init(context);
sessionCookie.put("hello", "session!");
// put nothing => intentionally to check if no session cookie will be
// saved
sessionCookie.save(context);
// a cookie will be set
verify(context).addCookie(cookieCaptor.capture());
// verify some stuff on the set cookie
assertEquals(true, cookieCaptor.getValue().isHttpOnly());
}
@Test
public void testNoHttpOnlyWorks() throws Exception {
// setup this testmethod
when(
ninjaProperties.getBooleanWithDefault(
NinjaConstant.sessionHttpOnly, true)).thenReturn(false);
Session sessionCookie = createNewSession();
sessionCookie.init(context);
sessionCookie.put("hello", "session!");
// put nothing => intentionally to check if no session cookie will be
// saved
sessionCookie.save(context);
// a cookie will be set
verify(context).addCookie(cookieCaptor.capture());
// verify some stuff on the set cookie
assertEquals(false, cookieCaptor.getValue().isHttpOnly());
}
@Test
public void testThatCookieSavingAndInitingWorks() {
Session sessionCookie = createNewSession();
sessionCookie.init(context);
sessionCookie.put("key1", "value1");
sessionCookie.put("key2", "value2");
sessionCookie.put("key3", "value3");
// put nothing => intentionally to check if no session cookie will be
// saved
sessionCookie.save(context);
// a cookie will be set
verify(context).addCookie(cookieCaptor.capture());
// now we simulate a new request => the session storage will generate a
// new cookie:
Cookie newSessionCookie = Cookie.builder(
cookieCaptor.getValue().getName(),
cookieCaptor.getValue().getValue()).build();
// that will be returned by the httprequest...
when(context.getCookie(cookieCaptor.getValue().getName())).thenReturn(
newSessionCookie);
// init new session from that cookie:
Session sessionCookie2 = createNewSession();
sessionCookie2.init(context);
assertEquals("value1", sessionCookie2.get("key1"));
assertEquals("value2", sessionCookie2.get("key2"));
assertEquals("value3", sessionCookie2.get("key3"));
}
@Test
public void testThatCorrectMethodOfNinjaPropertiesIsUsedSoThatStuffBreaksWhenPropertyIsAbsent() {
// we did not set the cookie prefix
when(ninjaProperties.getOrDie(NinjaConstant.applicationCookiePrefix))
.thenReturn(null);
// stuff must break => ...
Session sessionCookie = createNewSession();
verify(ninjaProperties).getOrDie(NinjaConstant.applicationCookiePrefix);
}
@Test
public void testSessionCookieDelete() {
Session sessionCookie = createNewSession();
sessionCookie.init(context);
final String key = "mykey";
final String value = "myvalue";
sessionCookie.put(key, value);
// value should have been set:
assertEquals(value, sessionCookie.get(key));
// value should be returned when removing:
assertEquals(value, sessionCookie.remove(key));
// after removing, value should not be there anymore:
assertNull(sessionCookie.get(key));
}
@Test
public void testGetAuthenticityTokenWorks() {
Session sessionCookie = createNewSession();
sessionCookie.init(context);
String authenticityToken = sessionCookie.getAuthenticityToken();
String cookieValueWithoutSign = captureFinalCookie(sessionCookie);
//verify that the authenticity token is set
assertTrue(cookieValueWithoutSign.contains(Session.AUTHENTICITY_KEY + "=" + authenticityToken));
// also make sure the timestamp is there:
assertTrue(cookieValueWithoutSign.contains(Session.TIMESTAMP_KEY));
}
@Test
public void testGetIdTokenWorks() {
Session sessionCookie = createNewSession();
sessionCookie.init(context);
String idToken = sessionCookie.getId();
String valueWithoutSign = captureFinalCookie(sessionCookie);
//verify that the id token is set:
assertTrue(valueWithoutSign.contains(Session.ID_KEY + "=" + idToken));
// also make sure the timestamp is there:
assertTrue(valueWithoutSign.contains(Session.TIMESTAMP_KEY));
}
@Test
public void testThatCookieUsesContextPath() {
Mockito.when(context.getContextPath()).thenReturn("/my_context");
Session sessionCookie = createNewSession();
sessionCookie.init(context);
sessionCookie.put("anykey", "anyvalue");
sessionCookie.save(context);
verify(context).addCookie(cookieCaptor.capture());
Cookie cookie = cookieCaptor.getValue();
assertThat(cookie.getPath(), CoreMatchers.equalTo("/my_context/"));
}
@Test
public void testExpiryTime() {
// 1. Check that session is still saved when expiry time is set
Session sessionCookie1 = createNewSession();
sessionCookie1.init(context);
sessionCookie1.put("a", "2");
sessionCookie1.setExpiryTime(10 * 1000L);
assertThat(sessionCookie1.get("a"), CoreMatchers.equalTo("2"));
sessionCookie1.save(context);
Session sessionCookie2 = roundTrip(sessionCookie1);
assertThat(sessionCookie2.get("a"), CoreMatchers.equalTo("2"));
// 2. Check that session is invalidated when past the expiry time
// Set the current time past when it is called.
when(clock.currentTimeMillis()).thenReturn(System.currentTimeMillis() + 11 * 1000L);
Session sessionCookie3 = roundTrip(sessionCookie2);
assertNull(sessionCookie3.get("a"));
}
@Test
public void testExpiryTimeRoundTrip() {
// Round trip the session cookie with an expiry time in the future
// Then remove the expiration time to make sure it is still valid
when(
ninjaProperties.getInteger(NinjaConstant.sessionExpireTimeInSeconds))
.thenReturn(null);
Session sessionCookie1 = createNewSession();
sessionCookie1.init(context);
sessionCookie1.put("a", "2");
sessionCookie1.setExpiryTime(10 * 1000L);
assertThat(sessionCookie1.get("a"), CoreMatchers.equalTo("2"));
Session sessionCookie2 = roundTrip(sessionCookie1);
assertThat(sessionCookie2.get("a"), CoreMatchers.equalTo("2"));
assertThat(sessionCookie2.get(Session.EXPIRY_TIME_KEY), CoreMatchers.equalTo("10000"));
sessionCookie2.setExpiryTime(null);
Session sessionCookie3 = roundTrip(sessionCookie2);
assertNull(sessionCookie3.get(Session.EXPIRY_TIME_KEY));
}
@Test
public void testThatCookieDoesNotUseApplicationDomainWhenNotSet() {
when(ninjaProperties.get(NinjaConstant.applicationCookieDomain)).thenReturn(null);
Session sessionCookie = createNewSession();
sessionCookie.init(context);
sessionCookie.put("anykey", "anyvalue");
sessionCookie.save(context);
verify(context).addCookie(cookieCaptor.capture());
Cookie cookie = cookieCaptor.getValue();
assertThat(cookie.getDomain(), CoreMatchers.equalTo(null));
}
@Test
public void testThatCookieUseApplicationDomain() {
when(ninjaProperties.get(NinjaConstant.applicationCookieDomain)).thenReturn("domain.com");
Session sessionCookie = createNewSession();
sessionCookie.init(context);
sessionCookie.put("anykey", "anyvalue");
sessionCookie.save(context);
verify(context).addCookie(cookieCaptor.capture());
Cookie cookie = cookieCaptor.getValue();
assertThat(cookie.getDomain(), CoreMatchers.equalTo("domain.com"));
}
@Test
public void testThatCookieClearWorks() {
String applicationCookieName = ninjaProperties.getOrDie(
NinjaConstant.applicationCookiePrefix)
+ ninja.utils.NinjaConstant.SESSION_SUFFIX;
// First roundtrip
Session sessionCookie = createNewSession();
sessionCookie.init(context);
sessionCookie.put("anykey", "anyvalue");
Session sessionCookieWithValues = roundTrip(sessionCookie);
// Second roundtrip with cleared session
sessionCookieWithValues.clear();
when(context.hasCookie(applicationCookieName)).thenReturn(true);
// Third roundtrip
String cookieValue = captureFinalCookie(sessionCookieWithValues);
assertThat(cookieValue, not(containsString("anykey")));
assertThat(cookieCaptor.getValue().getDomain(), CoreMatchers.equalTo(null));
assertThat(cookieCaptor.getValue().getMaxAge(), CoreMatchers.equalTo(0));
}
@Test
public void testThatCookieClearWorksWithApplicationDomain() {
String applicationCookieName = ninjaProperties.getOrDie(
NinjaConstant.applicationCookiePrefix)
+ ninja.utils.NinjaConstant.SESSION_SUFFIX;
when(ninjaProperties.get(NinjaConstant.applicationCookieDomain)).thenReturn("domain.com");
// First roundtrip
Session sessionCookie = createNewSession();
sessionCookie.init(context);
sessionCookie.put("anykey", "anyvalue");
Session sessionCookieWithValues = roundTrip(sessionCookie);
// Second roundtrip with cleared session
sessionCookieWithValues.clear();
when(context.hasCookie(applicationCookieName)).thenReturn(true);
// Third roundtrip
String cookieValue = captureFinalCookie(sessionCookieWithValues);
assertThat(cookieValue, not(containsString("anykey")));
assertThat(cookieCaptor.getValue().getDomain(), CoreMatchers.equalTo("domain.com"));
assertThat(cookieCaptor.getValue().getMaxAge(), CoreMatchers.equalTo(0));
}
@Test
public void testSessionEncryptionKeysMismatch() {
if (!encrypted) {
assertTrue("N/A for plain session cookies without encryption", true);
return;
}
// (1) create session with some data and save
Session session_1 = createNewSession();
session_1.init(context);
session_1.put("key", "value");
session_1.save(context);
// (2) verify that cookie with our data is created and added to context
verify(context).addCookie(cookieCaptor.capture());
assertEquals("value", session_1.get("key"));
// save reference to our cookie - we will use it to init sessions below
Cookie cookie = cookieCaptor.getValue();
// (3) create new session with the same cookie and assert that it still has our data
Session session_2 = createNewSession();
when(context.getCookie("NINJA_SESSION")).thenReturn(cookie);
session_2.init(context);
assertFalse(session_2.isEmpty());
assertEquals("value", session_2.get("key"));
// (4) now we change our application secret and thus our encryption key is modified
when(ninjaProperties.getOrDie(NinjaConstant.applicationSecret))
.thenReturn(SecretGenerator.generateSecret());
encryption = new CookieEncryption(ninjaProperties);
// (5) creating new session with the same cookie above would result in clean session
// because that cookie was encrypted with another key and decryption with the new key
// is not possible; usually such a case throws `javax.crypto.BadPaddingException`
Session session_3 = createNewSession();
session_3.init(context);
assertTrue(session_3.isEmpty());
}
private Session roundTrip(Session sessionCookie1) {
sessionCookie1.save(context);
// Get the cookie ...
verify(context, atLeastOnce()).addCookie(cookieCaptor.capture());
when(context.getCookie("NINJA_SESSION")).thenReturn(cookieCaptor.getValue());
// ... and roundtrip it into an new session
Session sessionCookie2 = createNewSession();
sessionCookie2.init(context);
return sessionCookie2;
}
private Session createNewSession() {
return new SessionImpl(crypto, encryption, ninjaProperties, clock);
}
private String captureFinalCookie(Session sessionCookie) {
sessionCookie.save(context);
// SessionImpl should set the cookie
verify(context, atLeastOnce()).addCookie(cookieCaptor.capture());
String cookieValue = cookieCaptor.getValue().getValue();
String cookieValueWithoutSign = cookieValue.substring(cookieValue.indexOf("-") + 1);
if (encrypted) {
cookieValueWithoutSign = encryption.decrypt(cookieValueWithoutSign);
}
return cookieValueWithoutSign;
}
}