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/TicTacToeServlet.java b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java
index 1f9581d9b2e..8eeffda4451 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
@@ -73,7 +73,11 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)
Game game = null;
String userId = userService.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 +106,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/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..7666e06792d
--- /dev/null
+++ b/appengine/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/TicTacToeServletTest.java
@@ -0,0 +1,225 @@
+/*
+ * 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.io.PrintWriter;
+import java.io.StringWriter;
+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;
+ private StringWriter responseWriter;
+ 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.
+ responseWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
+ when(mockRequest.getRequestURL()).thenReturn(new StringBuffer("https://timbre/"));
+ when(mockRequest.getRequestDispatcher("/WEB-INF/view/index.jsp")).thenReturn(requestDispatcher);
+
+ servletUnderTest = new TicTacToeServlet();
+ }
+
+ @After
+ public void tearDown() {
+ dbSession.close();
+ helper.tearDown();
+ }
+
+ @Test
+ public void doGet_loggedOut() throws Exception {
+ helper.setEnvIsLoggedIn(false);
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ String response = responseWriter.toString();
+ assertThat(response).contains("sign in");
+ }
+
+ @Test
+ public void doGet_loggedIn_noGameKey() throws Exception {
+ helper.setEnvIsLoggedIn(true);
+ // 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_loggedIn_existingGame() throws Exception {
+ helper.setEnvIsLoggedIn(true);
+ // 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_loggedIn_nonExistentGame() throws Exception {
+ helper.setEnvIsLoggedIn(true);
+
+ 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);
+ }
+}