diff --git a/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/DeleteServlet.java b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/DeleteServlet.java
index 7444191e419..5975aa191c4 100644
--- a/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/DeleteServlet.java
+++ b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/DeleteServlet.java
@@ -41,6 +41,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response)
UserService userService = UserServiceFactory.getUserService();
String currentUserId = userService.getCurrentUser().getUserId();
+ // TODO(you): In practice, first validate that the user has permission to delete the Game
game.deleteChannel(currentUserId);
}
}
diff --git a/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/FirebaseChannel.java b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/FirebaseChannel.java
index 7bebf9adcfa..a2903f4a6f3 100644
--- a/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/FirebaseChannel.java
+++ b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/FirebaseChannel.java
@@ -31,6 +31,7 @@
import java.io.FileInputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@@ -46,16 +47,18 @@
*/
public class FirebaseChannel {
private static final String FIREBASE_SNIPPET_PATH = "WEB-INF/view/firebase_config.jspf";
+ static InputStream firebaseConfigStream = null;
private static final Collection FIREBASE_SCOPES = Arrays.asList(
"https://www.googleapis.com/auth/firebase.database",
"https://www.googleapis.com/auth/userinfo.email"
);
private static final String IDENTITY_ENDPOINT =
"https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";
- static final HttpTransport HTTP_TRANSPORT = new UrlFetchTransport();
private String firebaseDbUrl;
private GoogleCredential credential;
+ // Keep this a package-private member variable, so that it can be mocked for unit tests
+ HttpTransport httpTransport;
private static FirebaseChannel instance;
@@ -79,11 +82,17 @@ public static FirebaseChannel getInstance() {
*/
private FirebaseChannel() {
try {
+ // This variables exist primarily so it can be stubbed out in unit tests.
+ if (null == firebaseConfigStream) {
+ firebaseConfigStream = new FileInputStream(FIREBASE_SNIPPET_PATH);
+ }
+
String firebaseSnippet = CharStreams.toString(new InputStreamReader(
- new FileInputStream(FIREBASE_SNIPPET_PATH), StandardCharsets.UTF_8));
+ firebaseConfigStream, StandardCharsets.UTF_8));
firebaseDbUrl = parseFirebaseUrl(firebaseSnippet);
credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
+ httpTransport = UrlFetchTransport.getDefaultInstance();
} catch (IOException e) {
throw new RuntimeException(e);
}
@@ -109,7 +118,7 @@ private static String parseFirebaseUrl(String firebaseSnippet) {
public void sendFirebaseMessage(String channelKey, Game game)
throws IOException {
// Make requests auth'ed using Application Default Credentials
- HttpRequestFactory requestFactory = HTTP_TRANSPORT.createRequestFactory(credential);
+ HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);
GenericUrl url = new GenericUrl(
String.format("%s/channels/%s.json", firebaseDbUrl, channelKey));
HttpResponse response = null;
diff --git a/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/MoveServlet.java b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/MoveServlet.java
index fffe9d8a15a..605749969c4 100644
--- a/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/MoveServlet.java
+++ b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/MoveServlet.java
@@ -44,7 +44,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response)
int cell = new Integer(request.getParameter("cell"));
if (!game.makeMove(cell, currentUserId)) {
- response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
} else {
ofy.save().entity(game).now();
}
diff --git a/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OpenedServlet.java b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OpenedServlet.java
index 1553b52da33..8a27c28ee10 100644
--- a/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OpenedServlet.java
+++ b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OpenedServlet.java
@@ -16,7 +16,6 @@
package com.example.appengine.firetactoe;
-import com.googlecode.objectify.NotFoundException;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyService;
@@ -32,19 +31,10 @@ public class OpenedServlet extends HttpServlet {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException {
+ // TODO(you): In practice, you should validate the user has permission to post to the given Game
String gameId = request.getParameter("gameKey");
Objectify ofy = ObjectifyService.ofy();
- try {
- Game game = ofy.load().type(Game.class).id(gameId).safe();
- if (gameId != null && request.getUserPrincipal() != null) {
- game.sendUpdateToClients();
- response.setContentType("text/plain");
- response.getWriter().println("ok");
- } else {
- response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
- }
- } catch (NotFoundException e) {
- response.setStatus(HttpServletResponse.SC_NOT_FOUND);
- }
+ Game game = ofy.load().type(Game.class).id(gameId).safe();
+ game.sendUpdateToClients();
}
}
diff --git a/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java
index 1f9581d9b2e..07e040b5244 100644
--- a/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java
+++ b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java
@@ -16,7 +16,6 @@
package com.example.appengine.firetactoe;
-import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.gson.Gson;
import com.googlecode.objectify.Objectify;
@@ -60,20 +59,18 @@ private String getGameUriWithGameParam(HttpServletRequest request, String gameKe
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
- final UserService userService = UserServiceFactory.getUserService();
String gameKey = request.getParameter("gameKey");
- if (userService.getCurrentUser() == null) {
- response.getWriter().println("
Please sign in.
");
- return;
- }
// 1. Create or fetch a Game object from the datastore
Objectify ofy = ObjectifyService.ofy();
Game game = null;
- String userId = userService.getCurrentUser().getUserId();
+ String userId = UserServiceFactory.getUserService().getCurrentUser().getUserId();
if (gameKey != null) {
- game = ofy.load().type(Game.class).id(gameKey).safe();
+ game = ofy.load().type(Game.class).id(gameKey).now();
+ if (null == game) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
if (game.getUserO() == null && !userId.equals(game.getUserX())) {
game.setUserO(userId);
}
@@ -102,6 +99,6 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)
request.setAttribute("channel_id", game.getChannelKey(userId));
request.setAttribute("initial_message", new Gson().toJson(game));
request.setAttribute("game_link", getGameUriWithGameParam(request, gameKey));
- getServletContext().getRequestDispatcher("/WEB-INF/view/index.jsp").forward(request, response);
+ request.getRequestDispatcher("/WEB-INF/view/index.jsp").forward(request, response);
}
}
diff --git a/appengine/firebase-tictactoe/src/main/webapp/WEB-INF/web.xml b/appengine/firebase-tictactoe/src/main/webapp/WEB-INF/web.xml
index fcc0ee9f00a..334b6e84ca8 100644
--- a/appengine/firebase-tictactoe/src/main/webapp/WEB-INF/web.xml
+++ b/appengine/firebase-tictactoe/src/main/webapp/WEB-INF/web.xml
@@ -21,6 +21,15 @@ http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
index
+
+
+ entire-app
+ /*
+
+
+ *
+
+
TicTacToeServlet
com.example.appengine.firetactoe.TicTacToeServlet
diff --git a/appengine/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/DeleteServletTest.java b/appengine/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/DeleteServletTest.java
new file mode 100644
index 00000000000..e89cff31704
--- /dev/null
+++ b/appengine/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/DeleteServletTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2016 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.example.appengine.firetactoe;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.appengine.tools.development.testing.LocalURLFetchServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig;
+import com.google.common.collect.ImmutableMap;
+import com.googlecode.objectify.Objectify;
+import com.googlecode.objectify.ObjectifyFactory;
+import com.googlecode.objectify.ObjectifyService;
+import com.googlecode.objectify.util.Closeable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link DeleteServlet}.
+ */
+@RunWith(JUnit4.class)
+public class DeleteServletTest {
+ private static final String USER_EMAIL = "whisky@tangofoxtr.ot";
+ private static final String USER_ID = "whiskytangofoxtrot";
+ private static final String FIREBASE_DB_URL = "http://firebase.com/dburl";
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Set no eventual consistency, that way queries return all results.
+ // http://g.co/cloud/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests
+ new LocalDatastoreServiceTestConfig().setDefaultHighRepJobPolicyUnappliedJobPercentage(0),
+ new LocalUserServiceTestConfig(),
+ new LocalURLFetchServiceTestConfig()
+ )
+ .setEnvEmail(USER_EMAIL)
+ .setEnvAuthDomain("gmail.com")
+ .setEnvAttributes(new HashMap(
+ ImmutableMap.of("com.google.appengine.api.users.UserService.user_id_key", USER_ID)));
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ protected Closeable dbSession;
+
+ private DeleteServlet servletUnderTest;
+
+ @BeforeClass
+ public static void setUpBeforeClass() {
+ // Reset the Factory so that all translators work properly.
+ ObjectifyService.setFactory(new ObjectifyFactory());
+ ObjectifyService.register(Game.class);
+ // Mock out the firebase config
+ FirebaseChannel.firebaseConfigStream = new ByteArrayInputStream(
+ String.format("databaseURL: \"%s\"", FIREBASE_DB_URL).getBytes());
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+ dbSession = ObjectifyService.begin();
+
+ servletUnderTest = new DeleteServlet();
+
+ helper.setEnvIsLoggedIn(true);
+ // Make sure there are no firebase requests if we don't expect it
+ FirebaseChannel.getInstance().httpTransport = null;
+ }
+
+ @After
+ public void tearDown() {
+ dbSession.close();
+ helper.tearDown();
+ }
+
+ @Test
+ public void doPost_noGameKey() throws Exception {
+ try {
+ servletUnderTest.doPost(mockRequest, mockResponse);
+ fail("Should not succeed with no gameKey specified.");
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage()).startsWith("id 'null'");
+ }
+ }
+
+ @Test
+ public void doPost_deleteGame() throws Exception {
+ // Insert a game
+ Objectify ofy = ObjectifyService.ofy();
+ Game game = new Game(USER_ID, "my-opponent", " ", true);
+ ofy.save().entity(game).now();
+ String gameKey = game.getId();
+ when(mockRequest.getParameter("gameKey")).thenReturn(gameKey);
+
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+
+ servletUnderTest.doPost(mockRequest, mockResponse);
+
+ verify(mockHttpTransport, times(1)).buildRequest(
+ eq("DELETE"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$"));
+ }
+}
diff --git a/appengine/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/MoveServletTest.java b/appengine/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/MoveServletTest.java
new file mode 100644
index 00000000000..5083c4c96c9
--- /dev/null
+++ b/appengine/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/MoveServletTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2016 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.example.appengine.firetactoe;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.appengine.tools.development.testing.LocalURLFetchServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig;
+import com.google.common.collect.ImmutableMap;
+import com.googlecode.objectify.Objectify;
+import com.googlecode.objectify.ObjectifyFactory;
+import com.googlecode.objectify.ObjectifyService;
+import com.googlecode.objectify.util.Closeable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link MoveServlet}.
+ */
+@RunWith(JUnit4.class)
+public class MoveServletTest {
+ private static final String USER_EMAIL = "whisky@tangofoxtr.ot";
+ private static final String USER_ID = "whiskytangofoxtrot";
+ private static final String FIREBASE_DB_URL = "http://firebase.com/dburl";
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Set no eventual consistency, that way queries return all results.
+ // http://g.co/cloud/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests
+ new LocalDatastoreServiceTestConfig().setDefaultHighRepJobPolicyUnappliedJobPercentage(0),
+ new LocalUserServiceTestConfig(),
+ new LocalURLFetchServiceTestConfig()
+ )
+ .setEnvEmail(USER_EMAIL)
+ .setEnvAuthDomain("gmail.com")
+ .setEnvAttributes(new HashMap(
+ ImmutableMap.of("com.google.appengine.api.users.UserService.user_id_key", USER_ID)));
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ protected Closeable dbSession;
+
+ private MoveServlet servletUnderTest;
+
+ @BeforeClass
+ public static void setUpBeforeClass() {
+ // Reset the Factory so that all translators work properly.
+ ObjectifyService.setFactory(new ObjectifyFactory());
+ ObjectifyService.register(Game.class);
+ // Mock out the firebase config
+ FirebaseChannel.firebaseConfigStream = new ByteArrayInputStream(
+ String.format("databaseURL: \"%s\"", FIREBASE_DB_URL).getBytes());
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+ dbSession = ObjectifyService.begin();
+
+ servletUnderTest = new MoveServlet();
+
+ helper.setEnvIsLoggedIn(true);
+ // Make sure there are no firebase requests if we don't expect it
+ FirebaseChannel.getInstance().httpTransport = null;
+ }
+
+ @After
+ public void tearDown() {
+ dbSession.close();
+ helper.tearDown();
+ }
+
+ @Test
+ public void doPost_myTurn_move() throws Exception {
+ // Insert a game
+ Objectify ofy = ObjectifyService.ofy();
+ Game game = new Game(USER_ID, "my-opponent", " ", true);
+ ofy.save().entity(game).now();
+ String gameKey = game.getId();
+
+ when(mockRequest.getParameter("gameKey")).thenReturn(gameKey);
+ when(mockRequest.getParameter("cell")).thenReturn("1");
+
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+
+ servletUnderTest.doPost(mockRequest, mockResponse);
+
+ game = ofy.load().type(Game.class).id(gameKey).safe();
+ assertThat(game.board).isEqualTo(" X ");
+
+ verify(mockHttpTransport, times(2)).buildRequest(
+ eq("PATCH"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$"));
+ }
+
+ public void doPost_notMyTurn_move() throws Exception {
+ // Insert a game
+ Objectify ofy = ObjectifyService.ofy();
+ Game game = new Game(USER_ID, "my-opponent", " ", false);
+ ofy.save().entity(game).now();
+ String gameKey = game.getId();
+
+ when(mockRequest.getParameter("gameKey")).thenReturn(gameKey);
+ when(mockRequest.getParameter("cell")).thenReturn("1");
+
+ servletUnderTest.doPost(mockRequest, mockResponse);
+
+ verify(mockResponse).sendError(401);
+ }
+}
diff --git a/appengine/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/OpenedServletTest.java b/appengine/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/OpenedServletTest.java
new file mode 100644
index 00000000000..593f4287988
--- /dev/null
+++ b/appengine/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/OpenedServletTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2016 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.example.appengine.firetactoe;
+
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.appengine.tools.development.testing.LocalURLFetchServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig;
+import com.google.common.collect.ImmutableMap;
+import com.googlecode.objectify.Objectify;
+import com.googlecode.objectify.ObjectifyFactory;
+import com.googlecode.objectify.ObjectifyService;
+import com.googlecode.objectify.util.Closeable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link OpenedServlet}.
+ */
+@RunWith(JUnit4.class)
+public class OpenedServletTest {
+ private static final String USER_EMAIL = "whisky@tangofoxtr.ot";
+ private static final String USER_ID = "whiskytangofoxtrot";
+ private static final String FIREBASE_DB_URL = "http://firebase.com/dburl";
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Set no eventual consistency, that way queries return all results.
+ // http://g.co/cloud/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests
+ new LocalDatastoreServiceTestConfig().setDefaultHighRepJobPolicyUnappliedJobPercentage(0),
+ new LocalUserServiceTestConfig(),
+ new LocalURLFetchServiceTestConfig()
+ )
+ .setEnvEmail(USER_EMAIL)
+ .setEnvAuthDomain("gmail.com")
+ .setEnvAttributes(new HashMap(
+ ImmutableMap.of("com.google.appengine.api.users.UserService.user_id_key", USER_ID)));
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ protected Closeable dbSession;
+
+ private OpenedServlet servletUnderTest;
+
+ @BeforeClass
+ public static void setUpBeforeClass() {
+ // Reset the Factory so that all translators work properly.
+ ObjectifyService.setFactory(new ObjectifyFactory());
+ ObjectifyService.register(Game.class);
+ // Mock out the firebase config
+ FirebaseChannel.firebaseConfigStream = new ByteArrayInputStream(
+ String.format("databaseURL: \"%s\"", FIREBASE_DB_URL).getBytes());
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+ dbSession = ObjectifyService.begin();
+
+ servletUnderTest = new OpenedServlet();
+
+ helper.setEnvIsLoggedIn(true);
+ // Make sure there are no firebase requests if we don't expect it
+ FirebaseChannel.getInstance().httpTransport = null;
+ }
+
+ @After
+ public void tearDown() {
+ dbSession.close();
+ helper.tearDown();
+ }
+
+ @Test
+ public void doPost_open() throws Exception {
+ // Insert a game
+ Objectify ofy = ObjectifyService.ofy();
+ Game game = new Game(USER_ID, "my-opponent", " ", true);
+ ofy.save().entity(game).now();
+ String gameKey = game.getId();
+
+ when(mockRequest.getParameter("gameKey")).thenReturn(gameKey);
+
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+
+ servletUnderTest.doPost(mockRequest, mockResponse);
+
+ verify(mockHttpTransport, times(2)).buildRequest(
+ eq("PATCH"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$"));
+ }
+}
diff --git a/appengine/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/TicTacToeServletTest.java b/appengine/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/TicTacToeServletTest.java
new file mode 100644
index 00000000000..aa45cba0875
--- /dev/null
+++ b/appengine/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/TicTacToeServletTest.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2016 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.example.appengine.firetactoe;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.appengine.tools.development.testing.LocalURLFetchServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig;
+import com.google.common.collect.ImmutableMap;
+import com.googlecode.objectify.Objectify;
+import com.googlecode.objectify.ObjectifyFactory;
+import com.googlecode.objectify.ObjectifyService;
+import com.googlecode.objectify.util.Closeable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.lang.StringBuffer;
+import java.util.HashMap;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link TicTacToeServlet}.
+ */
+@RunWith(JUnit4.class)
+public class TicTacToeServletTest {
+ private static final String USER_EMAIL = "whisky@tangofoxtr.ot";
+ private static final String USER_ID = "whiskytangofoxtrot";
+ private static final String FIREBASE_DB_URL = "http://firebase.com/dburl";
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Set no eventual consistency, that way queries return all results.
+ // http://g.co/cloud/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests
+ new LocalDatastoreServiceTestConfig().setDefaultHighRepJobPolicyUnappliedJobPercentage(0),
+ new LocalUserServiceTestConfig(),
+ new LocalURLFetchServiceTestConfig()
+ )
+ .setEnvEmail(USER_EMAIL)
+ .setEnvAuthDomain("gmail.com")
+ .setEnvAttributes(new HashMap(
+ ImmutableMap.of("com.google.appengine.api.users.UserService.user_id_key", USER_ID)));
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ protected Closeable dbSession;
+ @Mock RequestDispatcher requestDispatcher;
+
+ private TicTacToeServlet servletUnderTest;
+
+ @BeforeClass
+ public static void setUpBeforeClass() {
+ // Reset the Factory so that all translators work properly.
+ ObjectifyService.setFactory(new ObjectifyFactory());
+ ObjectifyService.register(Game.class);
+ // Mock out the firebase config
+ FirebaseChannel.firebaseConfigStream = new ByteArrayInputStream(
+ String.format("databaseURL: \"%s\"", FIREBASE_DB_URL).getBytes());
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+ dbSession = ObjectifyService.begin();
+
+ // Set up a fake HTTP response.
+ when(mockRequest.getRequestURL()).thenReturn(new StringBuffer("https://timbre/"));
+ when(mockRequest.getRequestDispatcher("/WEB-INF/view/index.jsp")).thenReturn(requestDispatcher);
+
+ servletUnderTest = new TicTacToeServlet();
+
+ helper.setEnvIsLoggedIn(true);
+ }
+
+ @After
+ public void tearDown() {
+ dbSession.close();
+ helper.tearDown();
+ }
+
+ @Test
+ public void doGet_noGameKey() throws Exception {
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ // Make sure the game object was created for a new game
+ Objectify ofy = ObjectifyService.ofy();
+ Game game = ofy.load().type(Game.class).first().safe();
+ assertThat(game.userX).isEqualTo(USER_ID);
+
+ verify(mockHttpTransport, times(1)).buildRequest(
+ eq("PATCH"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$"));
+ verify(requestDispatcher).forward(mockRequest, mockResponse);
+ verify(mockRequest).setAttribute(eq("token"), anyString());
+ verify(mockRequest).setAttribute("game_key", game.id);
+ verify(mockRequest).setAttribute("me", USER_ID);
+ verify(mockRequest).setAttribute("channel_id", USER_ID + game.id);
+ verify(mockRequest).setAttribute(eq("initial_message"), anyString());
+ verify(mockRequest).setAttribute(eq("game_link"), anyString());
+ }
+
+ @Test
+ public void doGet_existingGame() throws Exception {
+ // Mock out the firebase response. See
+ // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(200);
+ return response;
+ }
+ };
+ }
+ });
+ FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
+
+ // Insert a game
+ Objectify ofy = ObjectifyService.ofy();
+ Game game = new Game("some-other-user-id", null, " ", true);
+ ofy.save().entity(game).now();
+ String gameKey = game.getId();
+
+ when(mockRequest.getParameter("gameKey")).thenReturn(gameKey);
+
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ // Make sure the game object was updated with the other player
+ game = ofy.load().type(Game.class).first().safe();
+ assertThat(game.userX).isEqualTo("some-other-user-id");
+ assertThat(game.userO).isEqualTo(USER_ID);
+
+ verify(mockHttpTransport, times(2)).buildRequest(
+ eq("PATCH"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$"));
+ verify(requestDispatcher).forward(mockRequest, mockResponse);
+ verify(mockRequest).setAttribute(eq("token"), anyString());
+ verify(mockRequest).setAttribute("game_key", game.id);
+ verify(mockRequest).setAttribute("me", USER_ID);
+ verify(mockRequest).setAttribute("channel_id", USER_ID + gameKey);
+ verify(mockRequest).setAttribute(eq("initial_message"), anyString());
+ verify(mockRequest).setAttribute(eq("game_link"), anyString());
+ }
+
+ @Test
+ public void doGet_nonExistentGame() throws Exception {
+ when(mockRequest.getParameter("gameKey")).thenReturn("does-not-exist");
+
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ verify(mockResponse).sendError(404);
+ }
+}
diff --git a/unittests/pom.xml b/unittests/pom.xml
index 7b59f008c5d..37ef803e780 100644
--- a/unittests/pom.xml
+++ b/unittests/pom.xml
@@ -22,6 +22,7 @@
UTF-8
3.0.0
2.5.1
+ 1.22.0
@@ -66,6 +67,12 @@
${appengine.sdk.version}
test
+
+ com.google.api-client
+ google-api-client-appengine
+ ${google-api-client.version}
+ test
+
diff --git a/unittests/src/test/java/com/google/appengine/samples/LocalUrlFetchTest.java b/unittests/src/test/java/com/google/appengine/samples/LocalUrlFetchTest.java
new file mode 100644
index 00000000000..a2f1eba6c67
--- /dev/null
+++ b/unittests/src/test/java/com/google/appengine/samples/LocalUrlFetchTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016 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.appengine.samples;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.appengine.tools.development.testing.LocalURLFetchServiceTestConfig;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class LocalUrlFetchTest {
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(new LocalURLFetchServiceTestConfig());
+
+ @Before
+ public void setUp() {
+ helper.setUp();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void testMockUrlFetch() throws IOException {
+ // See http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
+ MockHttpTransport mockHttpTransport = new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ assertEquals(method, "GET");
+ assertEquals(url, "http://foo.bar");
+
+ return new MockLowLevelHttpRequest() {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+ response.setStatusCode(234);
+ return response;
+ }
+ };
+ }
+ };
+
+ HttpRequestFactory requestFactory = mockHttpTransport.createRequestFactory();
+ HttpResponse response = requestFactory.buildGetRequest(new GenericUrl("http://foo.bar"))
+ .execute();
+ assertEquals(response.getStatusCode(), 234);
+ }
+}