/**
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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 com.google.apphosting.runtime.jetty9;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertArrayEquals;
import com.google.appengine.api.NamespaceManager;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.DatastoreTimeoutException;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
import com.google.appengine.tools.development.testing.LocalMemcacheServiceTestConfig;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig;
import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig.DeferredTaskCallback;
import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig.TaskCountDownLatch;
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.ApiProxyException;
import com.google.apphosting.api.ApiProxy.Delegate;
import com.google.apphosting.api.ApiProxy.LogRecord;
import com.google.apphosting.runtime.DatastoreSessionStore;
import com.google.apphosting.runtime.DeferredDatastoreSessionStore;
import com.google.apphosting.runtime.MemcacheSessionStore;
import com.google.apphosting.runtime.SessionData;
import com.google.apphosting.runtime.SessionManagerUtil;
import com.google.apphosting.runtime.SessionStore;
import com.google.apphosting.runtime.jetty9.SessionManager.AppEngineSession;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import org.easymock.EasyMock;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* Tests for SessionManager and its inner classes.
*
* @author fabbott@google.com (Freeland Abbott)
*/
public class SessionManagerTest extends TestCase {
private static final int SESSION_EXPIRATION_SECONDS = 60;
private MemcacheService memcache;
private DatastoreService datastore;
private SessionManager manager;
private LocalServiceTestHelper helper =
new LocalServiceTestHelper(
new LocalDatastoreServiceTestConfig(), new LocalMemcacheServiceTestConfig());
private class TimeoutGeneratingDelegate implements Delegate<ApiProxy.Environment> {
private final Delegate delegate;
private int timeoutCount = 0;
private TimeoutGeneratingDelegate(Delegate delegate) {
this.delegate = delegate;
}
public void setTimeouts(int count) {
timeoutCount = count;
}
public int getTimeoutsRemaining() {
return timeoutCount;
}
@SuppressWarnings("unchecked")
public byte[] makeSyncCall(
ApiProxy.Environment environment, String packageName, String methodName, byte[] request)
throws ApiProxyException {
if (packageName.equals("datastore_v3") && timeoutCount > 0) {
timeoutCount--;
throw new DatastoreTimeoutException("Timeout");
}
return delegate.makeSyncCall(environment, packageName, methodName, request);
}
public Future<byte[]> makeAsyncCall(
ApiProxy.Environment environment,
String packageName,
String methodName,
byte[] request,
ApiProxy.ApiConfig apiConfig) {
if (packageName.equals("datastore_v3") && timeoutCount > 0) {
timeoutCount--;
throw new DatastoreTimeoutException("Timeout");
}
return delegate.makeAsyncCall(environment, packageName, methodName, request, apiConfig);
}
@SuppressWarnings("unchecked")
public void log(ApiProxy.Environment environment, LogRecord record) {
delegate.log(environment, record);
}
@SuppressWarnings("unchecked")
public void flushLogs(ApiProxy.Environment environment) {
delegate.flushLogs(environment);
}
public List<Thread> getRequestThreads(ApiProxy.Environment environment) {
return null;
}
}
public String startNamespace() {
return "";
}
public String testNamespace() {
return "";
}
public static class NamespacedStartTest extends SessionManagerTest {
@Override
public String startNamespace() {
return "start-namespace";
}
@Override
public String testNamespace() {
return "";
}
}
public static class NamespacedTest extends SessionManagerTest {
@Override
public String startNamespace() {
return "";
}
@Override
public String testNamespace() {
return "test-namespace";
}
}
public static class NamespacedTestTest extends SessionManagerTest {
@Override
public String startNamespace() {
return "start-namespace";
}
@Override
public String testNamespace() {
return "test-namespace";
}
}
@Override
public void setUp() throws Exception {
super.setUp();
helper.setUp();
memcache = MemcacheServiceFactory.getMemcacheService("");
datastore = DatastoreServiceFactory.getDatastoreService();
NamespaceManager.set(startNamespace());
manager =
new SessionManager(Arrays.asList(new DatastoreSessionStore(), new MemcacheSessionStore()));
NamespaceManager.set(testNamespace());
}
@Override
public void tearDown() throws Exception {
helper.tearDown();
super.tearDown();
}
public void testIdGeneration() {
long timestamp = System.currentTimeMillis();
SessionManager.SessionIdManager idManager = new SessionManager.SessionIdManager();
HttpServletRequest mockRequest = makeMockRequest(false);
replay(mockRequest);
String sessionid = idManager.newSessionId(mockRequest, timestamp);
assertNotNull(sessionid);
assertTrue(sessionid.length() > 0);
verify(mockRequest);
mockRequest = makeMockRequest(false);
replay(mockRequest);
String sessionid2 = idManager.newSessionId(mockRequest, timestamp);
assertNotNull(sessionid2);
assertTrue(sessionid2.length() > 0);
assertFalse(sessionid.equals(sessionid2));
verify(mockRequest);
}
@SuppressWarnings("unchecked")
public void testNewSession() throws EntityNotFoundException {
assertTrue(manager.getSessionIdManager() instanceof SessionManager.SessionIdManager);
manager.setMaxInactiveInterval(SESSION_EXPIRATION_SECONDS);
HttpServletRequest request = makeMockRequest(true);
replay(request);
AppEngineSession session = manager.newSession(request);
assertNotNull(session);
assertTrue(session instanceof SessionManager.AppEngineSession);
assertEquals(SessionManager.lastId(), session.getId());
session.setAttribute("foo", "bar");
session.save();
}
@SuppressWarnings("unchecked")
public void testGetSessionSameSessionManager() throws EntityNotFoundException {
HttpServletRequest request = makeMockRequest(true);
replay(request);
AppEngineSession session = manager.newSession(request);
session.setAttribute("foo", "bar");
assertEquals("bar", session.getAttribute("foo"));
session.save();
HttpSession session2 = manager.getSession(session.getId());
assertEquals(session.getId(), session2.getId());
assertEquals("bar", session2.getAttribute("foo"));
}
@SuppressWarnings("unchecked")
public void testGetSessionFromMemcache() throws EntityNotFoundException {
HttpServletRequest request = makeMockRequest(true);
replay(request);
AppEngineSession session = manager.newSession(request);
session.setAttribute("foo", "bar");
session.save();
// Ensure we're really fetching from memcache
SessionStore explosiveStore = EasyMock.createMock(SessionStore.class);
manager = new SessionManager(Arrays.asList(explosiveStore, new MemcacheSessionStore()));
HttpSession session2 = manager.getSession(session.getId());
assertEquals(session.getId(), session2.getId());
assertEquals("bar", session2.getAttribute("foo"));
}
public void testRenewSessionId() throws Exception {
HttpServletRequest request = makeMockRequest(true);
replay(request);
AppEngineSession session = manager.newSession(request);
session.setAttribute("foo", "bar");
session.save();
String oldId = session.getId();
byte[] bytes = (byte[])memcache.get(SessionManager.SESSION_PREFIX + oldId);
assertNotNull(bytes);
SessionData data = (SessionData)SessionManagerUtil.deserialize(bytes);
assertEquals("bar", data.getValueMap().get("foo"));
//renew session id
session.renewId(request);
//Ensure we deleted the session with the old id
AppEngineSession sessionx = manager.getSession(oldId);
assertNull(sessionx);
assertNull(memcache.get(SessionManager.SESSION_PREFIX + oldId));
//Ensure we changed the id
String newId = session.getId();
assertNotSame(oldId, newId);
//Ensure we stored the session with the new id
AppEngineSession session2 = manager.getSession(newId);
assertNotSame(session, session2);
assertEquals("bar", session2.getAttribute("foo"));
bytes = (byte[])memcache.get(SessionManager.SESSION_PREFIX + newId);
assertNotNull(bytes);
data = (SessionData)SessionManagerUtil.deserialize(bytes);
assertEquals("bar", data.getValueMap().get("foo"));
//Test we can store attributes
session2.setAttribute("one", "two");
session2.save();
bytes = (byte[])memcache.get(SessionManager.SESSION_PREFIX + newId);
assertNotNull(bytes);
data = (SessionData)SessionManagerUtil.deserialize(bytes);
assertEquals("two", data.getValueMap().get("one"));
}
private AppEngineSession createSession() {
NamespaceManager.set(startNamespace());
SessionManager localManager =
new SessionManager(Arrays.asList(new DatastoreSessionStore(), new MemcacheSessionStore()));
NamespaceManager.set(testNamespace());
HttpServletRequest request = makeMockRequest(true);
return localManager.newSession(request);
}
private HttpSession retrieveSession(AppEngineSession session) {
NamespaceManager.set(startNamespace());
SessionManager manager =
new SessionManager(Arrays.asList(new DatastoreSessionStore(), new MemcacheSessionStore()));
try {
return manager.getSession(session.getId());
} finally {
NamespaceManager.set(testNamespace());
}
}
private void testSerialize(String key, Object value) {
AppEngineSession session = createSession();
session.setAttribute(key, value);
session.save();
HttpSession httpSession = retrieveSession(session);
assertNotNull(httpSession);
Object result = httpSession.getAttribute(key);
if (!value.getClass().isArray()) {
assertEquals(value, result);
} else {
if (value instanceof Object[]) {
Object[] valueAsArray = (Object[]) value;
Object[] resultAsArray = (Object[]) result;
if (!Arrays.deepEquals(valueAsArray, resultAsArray)) {
throw new AssertionFailedError(
"Expected: "
+ Arrays.deepToString(valueAsArray)
+ " Received: "
+ Arrays.deepToString(resultAsArray));
}
} else if (value instanceof byte[]) {
assertArrayEquals((byte[]) value, (byte[]) result);
} else {
throw new RuntimeException("Unhandled array type, " + value.getClass());
}
}
}
private static class SomeSerializable implements Serializable {
int value;
public SomeSerializable(int value) {
this.value = value;
}
public boolean equals(Object obj) {
if (!(obj instanceof SomeSerializable)) {
return false;
}
return ((SomeSerializable) obj).value == value;
}
}
public void testSessionSerialization() throws EntityNotFoundException {
testSerialize("key", "value");
testSerialize("key", 1);
testSerialize("key", Math.PI);
testSerialize("key", new byte[] {1, 2, 3});
testSerialize("key", new String[] {"hello", "world"});
testSerialize(
"key",
new String[][] {
new String[] {"hello", "I"}, new String[] {"love", "you"}, new String[] {"won't", "you"},
});
testSerialize("key", new SomeSerializable(42));
testSerialize(
"key",
new Object[] {
new SomeSerializable(1), new SomeSerializable(2), new SomeSerializable(3),
});
testSerialize(
"key",
new SomeSerializable[] {
new SomeSerializable(1), new SomeSerializable(2), new SomeSerializable(3),
});
testSerialize("key", float.class);
Map<String, String> m = new HashMap<String, String>();
m.put("a", "b");
m.put("1", "2");
testSerialize("key", m);
// TODO(schwardo) or TODO(fabbot)
// Consider adding some cross-ClassLoader tests here.
}
private static class NonDeserializable implements Serializable {
int value;
public NonDeserializable(int value) {
this.value = value;
}
public boolean equals(Object obj) {
if (!(obj instanceof NonDeserializable)) {
return false;
}
return ((NonDeserializable) obj).value == value;
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
throw new ClassNotFoundException("cannot deserialize object");
}
}
public void testNonDeserializableSession() {
AppEngineSession session = createSession();
session.setAttribute("key", new NonDeserializable(1));
session.save();
assertNull(retrieveSession(session));
}
public void testSessionNotAvailableInMemcache() throws EntityNotFoundException {
HttpServletRequest request = makeMockRequest(true);
replay(request);
AppEngineSession session = manager.newSession(request);
session.setAttribute("foo", "bar");
session.save();
memcache.clearAll();
manager =
new SessionManager(Arrays.asList(new DatastoreSessionStore(), new MemcacheSessionStore()));
HttpSession session2 = manager.getSession(session.getId());
assertEquals(session.getId(), session2.getId());
assertEquals("bar", session2.getAttribute("foo"));
manager =
new SessionManager(Collections.<SessionStore>singletonList(new MemcacheSessionStore()));
assertNull(manager.getSession(session.getId()));
}
public void testGetSessionInvalid() throws EntityNotFoundException {
assertTrue(manager.getSessionIdManager() instanceof SessionManager.SessionIdManager);
manager.setMaxInactiveInterval(SESSION_EXPIRATION_SECONDS);
HttpSession session = manager.getSession("12345");
assertEquals(null, session);
}
@SuppressWarnings("unchecked")
public void testDatastoreTimeouts() throws EntityNotFoundException {
Delegate original = ApiProxy.getDelegate();
// Throw in a couple of datastore timeouts
TimeoutGeneratingDelegate newDelegate = new TimeoutGeneratingDelegate(original);
try {
ApiProxy.setDelegate(newDelegate);
HttpServletRequest request = makeMockRequest(true);
replay(request);
AppEngineSession session = manager.newSession(request);
session.setAttribute("foo", "bar");
newDelegate.setTimeouts(3);
session.save();
assertEquals(newDelegate.getTimeoutsRemaining(), 0);
memcache.clearAll();
manager =
new SessionManager(Collections.<SessionStore>singletonList(new DatastoreSessionStore()));
HttpSession session2 = manager.getSession(session.getId());
assertEquals(session.getId(), session2.getId());
assertEquals("bar", session2.getAttribute("foo"));
} finally {
ApiProxy.setDelegate(original);
}
}
public void testMemcacheOnlyLifecycle() {
manager =
new SessionManager(Collections.<SessionStore>singletonList(new MemcacheSessionStore()));
HttpServletRequest request = makeMockRequest(true);
replay(request);
AppEngineSession session = manager.newSession(request);
session.setAttribute("foo", "bar");
session.save();
assertNotNull(memcache.get(SessionManager.SESSION_PREFIX + session.getId()));
HttpSession session2 = manager.getSession(session.getId());
assertEquals(session.getId(), session2.getId());
assertEquals("bar", session2.getAttribute("foo"));
}
/** TODO Debug this
public void testDatastoreOnlyLifecycle() throws EntityNotFoundException {
manager =
new SessionManager(Collections.<SessionStore>singletonList(new DatastoreSessionStore()));
HttpServletRequest request = makeMockRequest(true);
replay(request);
AppEngineSession session = manager.newSession(request);
session.setAttribute("foo", "bar");
session.save();
Key key = KeyFactory.createKey("_ah_SESSION", SessionManager.SESSION_PREFIX + session.getId());
datastore.get(key);
HttpSession session2 = manager.getSession(session.getId());
assertEquals(session.getId(), session2.getId());
assertEquals("bar", session2.getAttribute("foo"));
}
*/
public void testDeferredDatastoreSessionStore()
throws InterruptedException, IOException, ClassNotFoundException, EntityNotFoundException {
helper.tearDown();
TaskCountDownLatch latch = new TaskCountDownLatch(1);
helper =
new LocalServiceTestHelper(
new LocalTaskQueueTestConfig()
.setCallbackClass(DeferredTaskCallback.class)
.setTaskExecutionLatch(latch)
.setDisableAutoTaskExecution(false),
new LocalDatastoreServiceTestConfig());
helper.setUp();
manager =
new SessionManager(
Collections.<SessionStore>singletonList(new DeferredDatastoreSessionStore(null)));
HttpServletRequest request = makeMockRequest(true);
replay(request);
AppEngineSession session = manager.newSession(request);
// Wait for the session creation task.
assertTrue(latch.awaitAndReset(10, TimeUnit.SECONDS));
session.setAttribute("foo", "bar");
session.save();
// Wait for the session update task.
assertTrue(latch.awaitAndReset(10, TimeUnit.SECONDS));
Key key = KeyFactory.createKey("_ah_SESSION", SessionManager.SESSION_PREFIX + session.getId());
datastore.get(key);
HttpSession session2 = manager.getSession(session.getId());
assertEquals(session.getId(), session2.getId());
assertEquals("bar", session2.getAttribute("foo"));
session.deleteSession();
// Wait for the session delete task.
assertTrue(latch.awaitAndReset(10, TimeUnit.SECONDS));
try {
datastore.get(key);
fail("entity should have been deleted");
} catch (EntityNotFoundException e) {
// good
}
}
private HttpServletRequest makeMockRequest(boolean forSession) {
HttpServletRequest mockRequest = createMock(HttpServletRequest.class);
if (forSession) {
expect(mockRequest.getAttribute("org.mortbay.http.ajp.JVMRoute")).andReturn(null).once();
}
return mockRequest;
}
}