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); + } +}