Skip to content

Commit 393e881

Browse files
author
Jerjou Cheng
committed
Add test to Firebase TicTacToe sample.
1 parent f2a628f commit 393e881

File tree

3 files changed

+243
-5
lines changed

3 files changed

+243
-5
lines changed

appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/FirebaseChannel.java

+12-3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import java.io.FileInputStream;
3333
import java.io.IOException;
34+
import java.io.InputStream;
3435
import java.io.InputStreamReader;
3536
import java.nio.charset.StandardCharsets;
3637
import java.util.Arrays;
@@ -46,16 +47,18 @@
4647
*/
4748
public class FirebaseChannel {
4849
private static final String FIREBASE_SNIPPET_PATH = "WEB-INF/view/firebase_config.jspf";
50+
static InputStream firebaseConfigStream = null;
4951
private static final Collection FIREBASE_SCOPES = Arrays.asList(
5052
"https://www.googleapis.com/auth/firebase.database",
5153
"https://www.googleapis.com/auth/userinfo.email"
5254
);
5355
private static final String IDENTITY_ENDPOINT =
5456
"https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";
55-
static final HttpTransport HTTP_TRANSPORT = new UrlFetchTransport();
5657

5758
private String firebaseDbUrl;
5859
private GoogleCredential credential;
60+
// Keep this a package-private member variable, so that it can be mocked for unit tests
61+
HttpTransport httpTransport;
5962

6063
private static FirebaseChannel instance;
6164

@@ -79,11 +82,17 @@ public static FirebaseChannel getInstance() {
7982
*/
8083
private FirebaseChannel() {
8184
try {
85+
// This variables exist primarily so it can be stubbed out in unit tests.
86+
if (null == firebaseConfigStream) {
87+
firebaseConfigStream = new FileInputStream(FIREBASE_SNIPPET_PATH);
88+
}
89+
8290
String firebaseSnippet = CharStreams.toString(new InputStreamReader(
83-
new FileInputStream(FIREBASE_SNIPPET_PATH), StandardCharsets.UTF_8));
91+
firebaseConfigStream, StandardCharsets.UTF_8));
8492
firebaseDbUrl = parseFirebaseUrl(firebaseSnippet);
8593

8694
credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
95+
httpTransport = UrlFetchTransport.getDefaultInstance();
8796
} catch (IOException e) {
8897
throw new RuntimeException(e);
8998
}
@@ -109,7 +118,7 @@ private static String parseFirebaseUrl(String firebaseSnippet) {
109118
public void sendFirebaseMessage(String channelKey, Game game)
110119
throws IOException {
111120
// Make requests auth'ed using Application Default Credentials
112-
HttpRequestFactory requestFactory = HTTP_TRANSPORT.createRequestFactory(credential);
121+
HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);
113122
GenericUrl url = new GenericUrl(
114123
String.format("%s/channels/%s.json", firebaseDbUrl, channelKey));
115124
HttpResponse response = null;

appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)
7373
Game game = null;
7474
String userId = userService.getCurrentUser().getUserId();
7575
if (gameKey != null) {
76-
game = ofy.load().type(Game.class).id(gameKey).safe();
76+
game = ofy.load().type(Game.class).id(gameKey).now();
77+
if (null == game) {
78+
response.sendError(HttpServletResponse.SC_NOT_FOUND);
79+
return;
80+
}
7781
if (game.getUserO() == null && !userId.equals(game.getUserX())) {
7882
game.setUserO(userId);
7983
}
@@ -102,6 +106,6 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)
102106
request.setAttribute("channel_id", game.getChannelKey(userId));
103107
request.setAttribute("initial_message", new Gson().toJson(game));
104108
request.setAttribute("game_link", getGameUriWithGameParam(request, gameKey));
105-
getServletContext().getRequestDispatcher("/WEB-INF/view/index.jsp").forward(request, response);
109+
request.getRequestDispatcher("/WEB-INF/view/index.jsp").forward(request, response);
106110
}
107111
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
/*
2+
* Copyright 2016 Google Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.appengine.firetactoe;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
import static org.mockito.Mockito.anyString;
21+
import static org.mockito.Mockito.eq;
22+
import static org.mockito.Mockito.spy;
23+
import static org.mockito.Mockito.times;
24+
import static org.mockito.Mockito.verify;
25+
import static org.mockito.Mockito.when;
26+
27+
import com.google.api.client.http.LowLevelHttpRequest;
28+
import com.google.api.client.http.LowLevelHttpResponse;
29+
import com.google.api.client.testing.http.MockHttpTransport;
30+
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
31+
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
32+
import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
33+
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
34+
import com.google.appengine.tools.development.testing.LocalURLFetchServiceTestConfig;
35+
import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig;
36+
import com.google.common.collect.ImmutableMap;
37+
import com.googlecode.objectify.Objectify;
38+
import com.googlecode.objectify.ObjectifyFactory;
39+
import com.googlecode.objectify.ObjectifyService;
40+
import com.googlecode.objectify.util.Closeable;
41+
import org.junit.After;
42+
import org.junit.Before;
43+
import org.junit.BeforeClass;
44+
import org.junit.Test;
45+
import org.junit.runner.RunWith;
46+
import org.junit.runners.JUnit4;
47+
import org.mockito.Matchers;
48+
import org.mockito.Mock;
49+
import org.mockito.MockitoAnnotations;
50+
51+
import java.io.ByteArrayInputStream;
52+
import java.io.IOException;
53+
import java.io.PrintWriter;
54+
import java.io.StringWriter;
55+
import java.lang.StringBuffer;
56+
import java.util.HashMap;
57+
import javax.servlet.RequestDispatcher;
58+
import javax.servlet.http.HttpServletRequest;
59+
import javax.servlet.http.HttpServletResponse;
60+
61+
/**
62+
* Unit tests for {@link TicTacToeServlet}.
63+
*/
64+
@RunWith(JUnit4.class)
65+
public class TicTacToeServletTest {
66+
private static final String USER_EMAIL = "[email protected]";
67+
private static final String USER_ID = "whiskytangofoxtrot";
68+
private static final String FIREBASE_DB_URL = "http://firebase.com/dburl";
69+
70+
private final LocalServiceTestHelper helper =
71+
new LocalServiceTestHelper(
72+
// Set no eventual consistency, that way queries return all results.
73+
// http://g.co/cloud/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests
74+
new LocalDatastoreServiceTestConfig().setDefaultHighRepJobPolicyUnappliedJobPercentage(0),
75+
new LocalUserServiceTestConfig(),
76+
new LocalURLFetchServiceTestConfig()
77+
)
78+
.setEnvEmail(USER_EMAIL)
79+
.setEnvAuthDomain("gmail.com")
80+
.setEnvAttributes(new HashMap(
81+
ImmutableMap.of("com.google.appengine.api.users.UserService.user_id_key", USER_ID)));
82+
83+
@Mock private HttpServletRequest mockRequest;
84+
@Mock private HttpServletResponse mockResponse;
85+
private StringWriter responseWriter;
86+
protected Closeable dbSession;
87+
@Mock RequestDispatcher requestDispatcher;
88+
89+
private TicTacToeServlet servletUnderTest;
90+
91+
@BeforeClass
92+
public static void setUpBeforeClass() {
93+
// Reset the Factory so that all translators work properly.
94+
ObjectifyService.setFactory(new ObjectifyFactory());
95+
ObjectifyService.register(Game.class);
96+
// Mock out the firebase config
97+
FirebaseChannel.firebaseConfigStream = new ByteArrayInputStream(
98+
String.format("databaseURL: \"%s\"", FIREBASE_DB_URL).getBytes());
99+
}
100+
101+
@Before
102+
public void setUp() throws Exception {
103+
MockitoAnnotations.initMocks(this);
104+
helper.setUp();
105+
dbSession = ObjectifyService.begin();
106+
107+
// Set up a fake HTTP response.
108+
responseWriter = new StringWriter();
109+
when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
110+
when(mockRequest.getRequestURL()).thenReturn(new StringBuffer("https://timbre/"));
111+
when(mockRequest.getRequestDispatcher("/WEB-INF/view/index.jsp")).thenReturn(requestDispatcher);
112+
113+
servletUnderTest = new TicTacToeServlet();
114+
}
115+
116+
@After
117+
public void tearDown() {
118+
dbSession.close();
119+
helper.tearDown();
120+
}
121+
122+
@Test
123+
public void doGet_loggedOut() throws Exception {
124+
helper.setEnvIsLoggedIn(false);
125+
servletUnderTest.doGet(mockRequest, mockResponse);
126+
127+
String response = responseWriter.toString();
128+
assertThat(response).contains("sign in");
129+
}
130+
131+
@Test
132+
public void doGet_loggedIn_noGameKey() throws Exception {
133+
helper.setEnvIsLoggedIn(true);
134+
// Mock out the firebase response. See
135+
// http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
136+
MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
137+
@Override
138+
public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
139+
return new MockLowLevelHttpRequest() {
140+
@Override
141+
public LowLevelHttpResponse execute() throws IOException {
142+
MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
143+
response.setStatusCode(200);
144+
return response;
145+
}
146+
};
147+
}
148+
});
149+
FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
150+
151+
servletUnderTest.doGet(mockRequest, mockResponse);
152+
153+
// Make sure the game object was created for a new game
154+
Objectify ofy = ObjectifyService.ofy();
155+
Game game = ofy.load().type(Game.class).first().safe();
156+
assertThat(game.userX).isEqualTo(USER_ID);
157+
158+
verify(mockHttpTransport, times(1)).buildRequest(
159+
eq("PATCH"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$"));
160+
verify(requestDispatcher).forward(mockRequest, mockResponse);
161+
verify(mockRequest).setAttribute(eq("token"), anyString());
162+
verify(mockRequest).setAttribute("game_key", game.id);
163+
verify(mockRequest).setAttribute("me", USER_ID);
164+
verify(mockRequest).setAttribute("channel_id", USER_ID + game.id);
165+
verify(mockRequest).setAttribute(eq("initial_message"), anyString());
166+
verify(mockRequest).setAttribute(eq("game_link"), anyString());
167+
}
168+
169+
@Test
170+
public void doGet_loggedIn_existingGame() throws Exception {
171+
helper.setEnvIsLoggedIn(true);
172+
// Mock out the firebase response. See
173+
// http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing
174+
MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() {
175+
@Override
176+
public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
177+
return new MockLowLevelHttpRequest() {
178+
@Override
179+
public LowLevelHttpResponse execute() throws IOException {
180+
MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
181+
response.setStatusCode(200);
182+
return response;
183+
}
184+
};
185+
}
186+
});
187+
FirebaseChannel.getInstance().httpTransport = mockHttpTransport;
188+
189+
// Insert a game
190+
Objectify ofy = ObjectifyService.ofy();
191+
Game game = new Game("some-other-user-id", null, " ", true);
192+
ofy.save().entity(game).now();
193+
String gameKey = game.getId();
194+
195+
when(mockRequest.getParameter("gameKey")).thenReturn(gameKey);
196+
197+
servletUnderTest.doGet(mockRequest, mockResponse);
198+
199+
// Make sure the game object was updated with the other player
200+
game = ofy.load().type(Game.class).first().safe();
201+
assertThat(game.userX).isEqualTo("some-other-user-id");
202+
assertThat(game.userO).isEqualTo(USER_ID);
203+
204+
verify(mockHttpTransport, times(2)).buildRequest(
205+
eq("PATCH"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$"));
206+
verify(requestDispatcher).forward(mockRequest, mockResponse);
207+
verify(mockRequest).setAttribute(eq("token"), anyString());
208+
verify(mockRequest).setAttribute("game_key", game.id);
209+
verify(mockRequest).setAttribute("me", USER_ID);
210+
verify(mockRequest).setAttribute("channel_id", USER_ID + gameKey);
211+
verify(mockRequest).setAttribute(eq("initial_message"), anyString());
212+
verify(mockRequest).setAttribute(eq("game_link"), anyString());
213+
}
214+
215+
@Test
216+
public void doGet_loggedIn_nonExistentGame() throws Exception {
217+
helper.setEnvIsLoggedIn(true);
218+
219+
when(mockRequest.getParameter("gameKey")).thenReturn("does-not-exist");
220+
221+
servletUnderTest.doGet(mockRequest, mockResponse);
222+
223+
verify(mockResponse).sendError(404);
224+
}
225+
}

0 commit comments

Comments
 (0)