Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(880)

Side by Side Diff: chrome/browser/chromeos/login/oauth2_browsertest.cc

Issue 108663008: Additional OAuth2 tests (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 7 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | google_apis/gaia/fake_gaia.h » ('j') | google_apis/gaia/fake_gaia.h » ('J')
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "base/message_loop/message_loop.h" 5 #include "base/message_loop/message_loop.h"
6 #include "base/prefs/pref_service.h"
7 #include "chrome/browser/browser_process.h"
6 #include "chrome/browser/chrome_notification_types.h" 8 #include "chrome/browser/chrome_notification_types.h"
7 #include "chrome/browser/chromeos/login/oauth2_login_manager.h" 9 #include "chrome/browser/chromeos/login/oauth2_login_manager.h"
8 #include "chrome/browser/chromeos/login/oauth2_login_manager_factory.h" 10 #include "chrome/browser/chromeos/login/oauth2_login_manager_factory.h"
9 #include "chrome/browser/chromeos/login/oobe_base_test.h" 11 #include "chrome/browser/chromeos/login/oobe_base_test.h"
12 #include "chrome/browser/chromeos/login/user_manager.h"
10 #include "chrome/browser/chromeos/login/wizard_controller.h" 13 #include "chrome/browser/chromeos/login/wizard_controller.h"
11 #include "chrome/browser/profiles/profile_manager.h" 14 #include "chrome/browser/profiles/profile_manager.h"
15 #include "chrome/browser/signin/profile_oauth2_token_service.h"
16 #include "chrome/browser/signin/profile_oauth2_token_service_factory.h"
12 #include "chrome/browser/ui/webui/chromeos/login/signin_screen_handler.h" 17 #include "chrome/browser/ui/webui/chromeos/login/signin_screen_handler.h"
13 #include "content/public/browser/notification_service.h" 18 #include "content/public/browser/notification_service.h"
14 #include "google_apis/gaia/gaia_constants.h" 19 #include "google_apis/gaia/gaia_constants.h"
15 #include "google_apis/gaia/gaia_urls.h" 20 #include "google_apis/gaia/gaia_urls.h"
21 #include "net/cookies/canonical_cookie.h"
22 #include "net/cookies/cookie_monster.h"
23 #include "net/cookies/cookie_store.h"
24 #include "net/url_request/url_request_context.h"
25 #include "net/url_request/url_request_context_getter.h"
16 26
17 namespace chromeos { 27 namespace chromeos {
18 28
19 namespace { 29 namespace {
20 30
21 // Email of owner account for test. 31 // Email of owner account for test.
22 const char kTestAccountId[] = "username@gmail.com"; 32 const char kTestAccountId[] = "username@gmail.com";
23 33 const char kTestRawAccountId[] = "User.Name";
34 const char kTestAccountPassword[] = "fake-password";
24 const char kTestAuthCode[] = "fake-auth-code"; 35 const char kTestAuthCode[] = "fake-auth-code";
25 const char kTestGaiaUberToken[] = "fake-uber-token"; 36 const char kTestGaiaUberToken[] = "fake-uber-token";
26 const char kTestAuthLoginAccessToken[] = "fake-access-token"; 37 const char kTestAuthLoginAccessToken[] = "fake-access-token";
27 const char kTestRefreshToken[] = "fake-refresh-token"; 38 const char kTestRefreshToken[] = "fake-refresh-token";
39 const char kTestAuthSIDCookie[] = "fake-auth-SID-cookie";
40 const char kTestAuthLSIDCookie[] = "fake-auth-LSID-cookie";
28 const char kTestSessionSIDCookie[] = "fake-session-SID-cookie"; 41 const char kTestSessionSIDCookie[] = "fake-session-SID-cookie";
29 const char kTestSessionLSIDCookie[] = "fake-session-LSID-cookie"; 42 const char kTestSessionLSIDCookie[] = "fake-session-LSID-cookie";
43 const char kTestSession2SIDCookie[] = "fake-session2-SID-cookie";
44 const char kTestSession2LSIDCookie[] = "fake-session2-LSID-cookie";
30 const char kTestUserinfoToken[] = "fake-userinfo-token"; 45 const char kTestUserinfoToken[] = "fake-userinfo-token";
31 const char kTestLoginToken[] = "fake-login-token"; 46 const char kTestLoginToken[] = "fake-login-token";
32 const char kTestAccessToken[] = "fake-access-token"; 47 const char kTestAccessToken[] = "fake-access-token";
33 const char kTestSyncToken[] = "fake-sync-token"; 48 const char kTestSyncToken[] = "fake-sync-token";
34 const char kTestAuthLoginToken[] = "fake-oauthlogin-token"; 49 const char kTestAuthLoginToken[] = "fake-oauthlogin-token";
35 const char kTestClientId[] = "fake-client-id"; 50 const char kTestClientId[] = "fake-client-id";
36 const char kTestAppScope[] = 51 const char kTestAppScope[] =
37 "https://www.googleapis.com/auth/userinfo.profile"; 52 "https://www.googleapis.com/auth/userinfo.profile";
38 53
39 } // namespace 54 } // namespace
40 55
41 class OAuth2Test : public OobeBaseTest { 56 class OAuth2Test : public OobeBaseTest {
42 protected: 57 protected:
43 OAuth2Test() {} 58 OAuth2Test() {}
44 59
45 virtual void SetUpOnMainThread() OVERRIDE { 60 virtual void SetUpOnMainThread() OVERRIDE {
46 OobeBaseTest::SetUpOnMainThread(); 61 OobeBaseTest::SetUpOnMainThread();
62 }
47 63
64 void SetupGaiaServerForNewAccount() {
65 FakeGaia::MergeSessionParams params;
66 params.auth_sid_cookie = kTestAuthSIDCookie;
67 params.auth_lsid_cookie = kTestAuthLSIDCookie;
68 params.auth_code = kTestAuthCode;
69 params.refresh_token = kTestRefreshToken;
70 params.access_token = kTestAuthLoginAccessToken;
71 params.gaia_uber_token = kTestGaiaUberToken;
72 params.session_sid_cookie = kTestSessionSIDCookie;
73 params.session_lsid_cookie = kTestSessionLSIDCookie;
74 fake_gaia_.SetMergeSessionParams(params);
75 SetupGaiaServerWithAccessTokens();
76 }
77
78 void SetupGaiaServerForExistingAccount() {
79 FakeGaia::MergeSessionParams params;
80 params.gaia_uber_token = kTestGaiaUberToken;
81 params.session_sid_cookie = kTestSession2SIDCookie;
82 params.session_lsid_cookie = kTestSession2LSIDCookie;
83 fake_gaia_.SetMergeSessionParams(params);
84 SetupGaiaServerWithAccessTokens();
85 }
86
87 bool TryToLogin(const std::string& username,
88 const std::string& password) {
89 if (!AddUserTosession(username, password))
90 return false;
91
92 if (const User* active_user = UserManager::Get()->GetActiveUser())
93 return active_user->email() == username;
94
95 return false;
96 }
97
98 User::OAuthTokenStatus GetOAuthStatusFromLocalState(
99 const std::string& user_id) const {
100 PrefService* local_state = g_browser_process->local_state();
101 const DictionaryValue* prefs_oauth_status =
102 local_state->GetDictionary("OAuthTokenStatus");
103 int oauth_token_status = User::OAUTH_TOKEN_STATUS_UNKNOWN;
104 if (prefs_oauth_status &&
105 prefs_oauth_status->GetIntegerWithoutPathExpansion(
106 user_id, &oauth_token_status)) {
107 User::OAuthTokenStatus result =
108 static_cast<User::OAuthTokenStatus>(oauth_token_status);
109 return result;
110 }
111 return User::OAUTH_TOKEN_STATUS_UNKNOWN;
112 }
113
114 private:
115 bool AddUserTosession(const std::string& username,
116 const std::string& password) {
117 ExistingUserController* controller =
118 ExistingUserController::current_controller();
119 if (!controller) {
120 ADD_FAILURE();
121 return false;
122 }
123
124 controller->Login(UserContext(username, password, std::string()));
125 content::WindowedNotificationObserver(
126 chrome::NOTIFICATION_SESSION_STARTED,
127 content::NotificationService::AllSources()).Wait();
128 const UserList& logged_users = UserManager::Get()->GetLoggedInUsers();
129 for (UserList::const_iterator it = logged_users.begin();
130 it != logged_users.end(); ++it) {
131 if ((*it)->email() == username)
132 return true;
133 }
134 return false;
135 }
136
137 void SetupGaiaServerWithAccessTokens() {
48 // Configure OAuth authentication. 138 // Configure OAuth authentication.
49 GaiaUrls* gaia_urls = GaiaUrls::GetInstance(); 139 GaiaUrls* gaia_urls = GaiaUrls::GetInstance();
50 140
51 fake_gaia_.SetAuthTokens(kTestAuthCode,
52 kTestRefreshToken,
53 kTestAuthLoginAccessToken,
54 kTestGaiaUberToken,
55 kTestSessionSIDCookie,
56 kTestSessionLSIDCookie);
57 // This token satisfies the userinfo.email request from 141 // This token satisfies the userinfo.email request from
58 // DeviceOAuth2TokenService used in token validation. 142 // DeviceOAuth2TokenService used in token validation.
59 FakeGaia::AccessTokenInfo userinfo_token_info; 143 FakeGaia::AccessTokenInfo userinfo_token_info;
60 userinfo_token_info.token = kTestUserinfoToken; 144 userinfo_token_info.token = kTestUserinfoToken;
61 userinfo_token_info.scopes.insert( 145 userinfo_token_info.scopes.insert(
62 "https://www.googleapis.com/auth/userinfo.email"); 146 "https://www.googleapis.com/auth/userinfo.email");
63 userinfo_token_info.audience = gaia_urls->oauth2_chrome_client_id(); 147 userinfo_token_info.audience = gaia_urls->oauth2_chrome_client_id();
64 userinfo_token_info.email = kTestAccountId; 148 userinfo_token_info.email = kTestAccountId;
65 fake_gaia_.IssueOAuthToken(kTestRefreshToken, userinfo_token_info); 149 fake_gaia_.IssueOAuthToken(kTestRefreshToken, userinfo_token_info);
66 150
(...skipping 19 matching lines...) Expand all
86 sync_token_info.audience = gaia_urls->oauth2_chrome_client_id(); 170 sync_token_info.audience = gaia_urls->oauth2_chrome_client_id();
87 fake_gaia_.IssueOAuthToken(kTestRefreshToken, sync_token_info); 171 fake_gaia_.IssueOAuthToken(kTestRefreshToken, sync_token_info);
88 172
89 FakeGaia::AccessTokenInfo auth_login_token_info; 173 FakeGaia::AccessTokenInfo auth_login_token_info;
90 auth_login_token_info.token = kTestAuthLoginToken; 174 auth_login_token_info.token = kTestAuthLoginToken;
91 auth_login_token_info.scopes.insert(gaia_urls->oauth1_login_scope()); 175 auth_login_token_info.scopes.insert(gaia_urls->oauth1_login_scope());
92 auth_login_token_info.audience = gaia_urls->oauth2_chrome_client_id(); 176 auth_login_token_info.audience = gaia_urls->oauth2_chrome_client_id();
93 fake_gaia_.IssueOAuthToken(kTestRefreshToken, auth_login_token_info); 177 fake_gaia_.IssueOAuthToken(kTestRefreshToken, auth_login_token_info);
94 } 178 }
95 179
180 DISALLOW_COPY_AND_ASSIGN(OAuth2Test);
181 };
182
183 class CookieReader : public base::RefCountedThreadSafe<CookieReader> {
184 public:
185 CookieReader() {
186 }
187
188 virtual ~CookieReader() {
189 }
190
191 void ReadCookies(Profile* profile) {
192 context_ = profile->GetRequestContext();
193 content::BrowserThread::PostTask(
194 content::BrowserThread::IO, FROM_HERE,
195 base::Bind(&CookieReader::ReadCookiesOnIOThread,
196 this));
197 runner_ = new content::MessageLoopRunner;
198 runner_->Run();
199 }
200
201 std::string GetCookieValue(const std::string& name) {
202 for (std::vector<net::CanonicalCookie>::const_iterator iter =
203 cookie_list_.begin();
204 iter != cookie_list_.end();
205 ++iter) {
206 if (iter->Name() == name) {
207 return iter->Value();
208 }
209 }
210 return std::string();
211 }
212
96 private: 213 private:
97 DISALLOW_COPY_AND_ASSIGN(OAuth2Test); 214 void ReadCookiesOnIOThread() {
215 context_->GetURLRequestContext()->cookie_store()->GetCookieMonster()->
216 GetAllCookiesAsync(base::Bind(
217 &CookieReader::OnGetAllCookiesOnUIThread,
218 this));
219 }
220
221 void OnGetAllCookiesOnUIThread(const net::CookieList& cookies) {
222 cookie_list_ = cookies;
223 content::BrowserThread::PostTask(
224 content::BrowserThread::UI, FROM_HERE,
225 base::Bind(&CookieReader::OnCookiesReadyOnUIThread,
226 this));
227 }
228
229 void OnCookiesReadyOnUIThread() {
230 runner_->Quit();
231 }
232
233 scoped_refptr<net::URLRequestContextGetter> context_;
234 net::CookieList cookie_list_;
235 scoped_refptr<content::MessageLoopRunner> runner_;
98 }; 236 };
xiyuan 2013/12/17 17:12:57 nit: DISALLOW_COPY_AND_ASSIGN
zel 2013/12/17 17:23:15 Done.
99 237
100 class OAuth2LoginManagerStateWaiter : public OAuth2LoginManager::Observer { 238 class OAuth2LoginManagerStateWaiter : public OAuth2LoginManager::Observer {
101 public: 239 public:
102 explicit OAuth2LoginManagerStateWaiter(Profile* profile) 240 explicit OAuth2LoginManagerStateWaiter(Profile* profile)
103 : profile_(profile), 241 : profile_(profile),
104 waiting_for_state_(false), 242 waiting_for_state_(false),
105 final_state_(OAuth2LoginManager::SESSION_RESTORE_NOT_STARTED) { 243 final_state_(OAuth2LoginManager::SESSION_RESTORE_NOT_STARTED) {
106 } 244 }
107 245
108 void WaitForStates( 246 void WaitForStates(
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after
143 281
144 Profile* profile_; 282 Profile* profile_;
145 std::set<OAuth2LoginManager::SessionRestoreState> states_; 283 std::set<OAuth2LoginManager::SessionRestoreState> states_;
146 bool waiting_for_state_; 284 bool waiting_for_state_;
147 OAuth2LoginManager::SessionRestoreState final_state_; 285 OAuth2LoginManager::SessionRestoreState final_state_;
148 scoped_refptr<content::MessageLoopRunner> runner_; 286 scoped_refptr<content::MessageLoopRunner> runner_;
149 287
150 DISALLOW_COPY_AND_ASSIGN(OAuth2LoginManagerStateWaiter); 288 DISALLOW_COPY_AND_ASSIGN(OAuth2LoginManagerStateWaiter);
151 }; 289 };
152 290
153 IN_PROC_BROWSER_TEST_F(OAuth2Test, NewUser) { 291 // PRE_MergeSession is testing merge session for a new profile.
292 IN_PROC_BROWSER_TEST_F(OAuth2Test, PRE_PRE_MergeSession) {
293 SetupGaiaServerForNewAccount();
154 SimulateNetworkOnline(); 294 SimulateNetworkOnline();
155 chromeos::WizardController::SkipPostLoginScreensForTesting(); 295 chromeos::WizardController::SkipPostLoginScreensForTesting();
156 chromeos::WizardController* wizard_controller = 296 chromeos::WizardController* wizard_controller =
157 chromeos::WizardController::default_controller(); 297 chromeos::WizardController::default_controller();
158 wizard_controller->SkipToLoginForTesting(LoginScreenContext()); 298 wizard_controller->SkipToLoginForTesting(LoginScreenContext());
159 299
160 content::WindowedNotificationObserver( 300 content::WindowedNotificationObserver(
161 chrome::NOTIFICATION_LOGIN_OR_LOCK_WEBUI_VISIBLE, 301 chrome::NOTIFICATION_LOGIN_OR_LOCK_WEBUI_VISIBLE,
162 content::NotificationService::AllSources()).Wait(); 302 content::NotificationService::AllSources()).Wait();
163 303
304 EXPECT_EQ(GetOAuthStatusFromLocalState(kTestAccountId),
305 User::OAUTH_TOKEN_STATUS_UNKNOWN);
306
164 // Use capitalized and dotted user name on purpose to make sure 307 // Use capitalized and dotted user name on purpose to make sure
165 // our email normalization kicks in. 308 // our email normalization kicks in.
166 GetLoginDisplay()->ShowSigninScreenForCreds("User.Name", "password"); 309 GetLoginDisplay()->ShowSigninScreenForCreds(kTestRawAccountId,
310 kTestAccountPassword);
167 311
168 content::WindowedNotificationObserver( 312 content::WindowedNotificationObserver(
169 chrome::NOTIFICATION_SESSION_STARTED, 313 chrome::NOTIFICATION_SESSION_STARTED,
170 content::NotificationService::AllSources()).Wait(); 314 content::NotificationService::AllSources()).Wait();
315 Profile* profile = ProfileManager::GetPrimaryUserProfile();
171 316
317 // Wait for the session merge to finish.
172 std::set<OAuth2LoginManager::SessionRestoreState> states; 318 std::set<OAuth2LoginManager::SessionRestoreState> states;
173 states.insert(OAuth2LoginManager::SESSION_RESTORE_DONE); 319 states.insert(OAuth2LoginManager::SESSION_RESTORE_DONE);
174 states.insert(OAuth2LoginManager::SESSION_RESTORE_FAILED); 320 states.insert(OAuth2LoginManager::SESSION_RESTORE_FAILED);
175 states.insert(OAuth2LoginManager::SESSION_RESTORE_CONNECTION_FAILED); 321 states.insert(OAuth2LoginManager::SESSION_RESTORE_CONNECTION_FAILED);
176 OAuth2LoginManagerStateWaiter merge_session_waiter( 322 OAuth2LoginManagerStateWaiter merge_session_waiter(
177 ProfileManager::GetPrimaryUserProfile()); 323 ProfileManager::GetPrimaryUserProfile());
178 merge_session_waiter.WaitForStates(states); 324 merge_session_waiter.WaitForStates(states);
179 EXPECT_EQ(merge_session_waiter.final_state(), 325 EXPECT_EQ(merge_session_waiter.final_state(),
180 OAuth2LoginManager::SESSION_RESTORE_DONE); 326 OAuth2LoginManager::SESSION_RESTORE_DONE);
327
328 // Check for existance of refresh token.
329 ProfileOAuth2TokenService* token_service =
330 ProfileOAuth2TokenServiceFactory::GetForProfile(
331 profile);
332 EXPECT_TRUE(token_service->RefreshTokenIsAvailable(kTestAccountId));
333
334 EXPECT_EQ(GetOAuthStatusFromLocalState(kTestAccountId),
335 User::OAUTH2_TOKEN_STATUS_VALID);
336
337 scoped_refptr<CookieReader> cookie_reader(new CookieReader());
338 cookie_reader->ReadCookies(profile);
339 EXPECT_EQ(cookie_reader->GetCookieValue("SID"), kTestSessionSIDCookie);
340 EXPECT_EQ(cookie_reader->GetCookieValue("LSID"), kTestSessionLSIDCookie);
341 }
342
343 // MergeSession test is running merge session process for an existing profile
344 // that was generated in PRE_MergeSession test.
345 IN_PROC_BROWSER_TEST_F(OAuth2Test, PRE_MergeSession) {
346 SetupGaiaServerForExistingAccount();
347 SimulateNetworkOnline();
348
349 content::WindowedNotificationObserver(
350 chrome::NOTIFICATION_LOGIN_OR_LOCK_WEBUI_VISIBLE,
351 content::NotificationService::AllSources()).Wait();
352
353 JsExpect("!!document.querySelector('#account-picker')");
354 JsExpect("!!document.querySelector('#pod-row')");
355
356 EXPECT_EQ(GetOAuthStatusFromLocalState(kTestAccountId),
357 User::OAUTH2_TOKEN_STATUS_VALID);
358
359 EXPECT_TRUE(TryToLogin(kTestAccountId, kTestAccountPassword));
360 Profile* profile = ProfileManager::GetPrimaryUserProfile();
361
362 // Wait for the session merge to finish.
363 std::set<OAuth2LoginManager::SessionRestoreState> states;
364 states.insert(OAuth2LoginManager::SESSION_RESTORE_DONE);
365 states.insert(OAuth2LoginManager::SESSION_RESTORE_FAILED);
366 states.insert(OAuth2LoginManager::SESSION_RESTORE_CONNECTION_FAILED);
367 OAuth2LoginManagerStateWaiter merge_session_waiter(profile);
368 merge_session_waiter.WaitForStates(states);
369 EXPECT_EQ(merge_session_waiter.final_state(),
370 OAuth2LoginManager::SESSION_RESTORE_DONE);
371
372 // Check for existance of refresh token.
373 ProfileOAuth2TokenService* token_service =
374 ProfileOAuth2TokenServiceFactory::GetForProfile(profile);
375 EXPECT_TRUE(token_service->RefreshTokenIsAvailable(kTestAccountId));
376
377 EXPECT_EQ(GetOAuthStatusFromLocalState(kTestAccountId),
378 User::OAUTH2_TOKEN_STATUS_VALID);
379
380 scoped_refptr<CookieReader> cookie_reader(new CookieReader());
381 cookie_reader->ReadCookies(profile);
382 EXPECT_EQ(cookie_reader->GetCookieValue("SID"), kTestSession2SIDCookie);
383 EXPECT_EQ(cookie_reader->GetCookieValue("LSID"), kTestSession2LSIDCookie);
384 }
385
386 // MergeSession test is running merge session process for an existing profile
387 // that was generated in PRE_MergeSession test.
xiyuan 2013/12/17 17:12:57 nit: Comment why merge session fails in this test.
zel 2013/12/17 17:23:15 Done.
388 IN_PROC_BROWSER_TEST_F(OAuth2Test, MergeSession) {
389 SimulateNetworkOnline();
390
391 content::WindowedNotificationObserver(
392 chrome::NOTIFICATION_LOGIN_OR_LOCK_WEBUI_VISIBLE,
393 content::NotificationService::AllSources()).Wait();
394
395 JsExpect("!!document.querySelector('#account-picker')");
396 JsExpect("!!document.querySelector('#pod-row')");
397
398 EXPECT_EQ(GetOAuthStatusFromLocalState(kTestAccountId),
399 User::OAUTH2_TOKEN_STATUS_VALID);
400
401 EXPECT_TRUE(TryToLogin(kTestAccountId, kTestAccountPassword));
402
403 // Wait for the session merge to finish.
404 std::set<OAuth2LoginManager::SessionRestoreState> states;
405 states.insert(OAuth2LoginManager::SESSION_RESTORE_DONE);
406 states.insert(OAuth2LoginManager::SESSION_RESTORE_FAILED);
407 states.insert(OAuth2LoginManager::SESSION_RESTORE_CONNECTION_FAILED);
408 OAuth2LoginManagerStateWaiter merge_session_waiter(
409 ProfileManager::GetPrimaryUserProfile());
410 merge_session_waiter.WaitForStates(states);
411 EXPECT_EQ(merge_session_waiter.final_state(),
412 OAuth2LoginManager::SESSION_RESTORE_FAILED);
413
414 EXPECT_EQ(GetOAuthStatusFromLocalState(kTestAccountId),
415 User::OAUTH2_TOKEN_STATUS_INVALID);
181 } 416 }
182 417
183 } // namespace chromeos 418 } // namespace chromeos
OLDNEW
« no previous file with comments | « no previous file | google_apis/gaia/fake_gaia.h » ('j') | google_apis/gaia/fake_gaia.h » ('J')

Powered by Google App Engine
This is Rietveld 408576698