Skip to content

Commit

Permalink
Add test to Firebase TicTacToe sample.
Browse files Browse the repository at this point in the history
  • Loading branch information
Jerjou Cheng committed Oct 19, 2016
1 parent f2a628f commit 393e881
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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);
}
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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 = "[email protected]";
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);
}
}

0 comments on commit 393e881

Please sign in to comment.