From 393e881d1e88bcfb3e23f99637e5ded848209f1f Mon Sep 17 00:00:00 2001 From: Jerjou Cheng Date: Wed, 19 Oct 2016 11:19:41 -0700 Subject: [PATCH] Add test to Firebase TicTacToe sample. --- .../appengine/firetactoe/FirebaseChannel.java | 15 +- .../firetactoe/TicTacToeServlet.java | 8 +- .../firetactoe/TicTacToeServletTest.java | 225 ++++++++++++++++++ 3 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 appengine/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/TicTacToeServletTest.java 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); + } +}