// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/extensions/extension_apitest.h" #include "base/strings/string_split.h" #include "base/strings/string_util.h" #include "base/strings/stringprintf.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/extensions/api/test/test_api.h" #include "chrome/browser/extensions/extension_service.h" #include "chrome/browser/extensions/extension_system.h" #include "chrome/browser/extensions/unpacked_installer.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/extensions/application_launch.h" #include "chrome/test/base/ui_test_utils.h" #include "content/public/browser/notification_registrar.h" #include "content/public/browser/notification_service.h" #include "extensions/common/extension.h" #include "net/base/escape.h" #include "net/base/net_util.h" #include "net/test/embedded_test_server/embedded_test_server.h" #include "net/test/embedded_test_server/http_request.h" #include "net/test/embedded_test_server/http_response.h" #include "net/test/spawned_test_server/spawned_test_server.h" namespace { const char kTestCustomArg[] = "customArg"; const char kTestServerPort[] = "testServer.port"; const char kTestDataDirectory[] = "testDataDirectory"; const char kTestWebSocketPort[] = "testWebSocketPort"; const char kSpawnedTestServerPort[] = "spawnedTestServer.port"; scoped_ptr<net::test_server::HttpResponse> HandleServerRedirectRequest( const net::test_server::HttpRequest& request) { if (!StartsWithASCII(request.relative_url, "/server-redirect?", true)) return scoped_ptr<net::test_server::HttpResponse>(); size_t query_string_pos = request.relative_url.find('?'); std::string redirect_target = request.relative_url.substr(query_string_pos + 1); scoped_ptr<net::test_server::BasicHttpResponse> http_response( new net::test_server::BasicHttpResponse); http_response->set_code(net::HTTP_MOVED_PERMANENTLY); http_response->AddCustomHeader("Location", redirect_target); return http_response.PassAs<net::test_server::HttpResponse>(); } scoped_ptr<net::test_server::HttpResponse> HandleEchoHeaderRequest( const net::test_server::HttpRequest& request) { if (!StartsWithASCII(request.relative_url, "/echoheader?", true)) return scoped_ptr<net::test_server::HttpResponse>(); size_t query_string_pos = request.relative_url.find('?'); std::string header_name = request.relative_url.substr(query_string_pos + 1); std::string header_value; std::map<std::string, std::string>::const_iterator it = request.headers.find( header_name); if (it != request.headers.end()) header_value = it->second; scoped_ptr<net::test_server::BasicHttpResponse> http_response( new net::test_server::BasicHttpResponse); http_response->set_code(net::HTTP_OK); http_response->set_content(header_value); return http_response.PassAs<net::test_server::HttpResponse>(); } scoped_ptr<net::test_server::HttpResponse> HandleSetCookieRequest( const net::test_server::HttpRequest& request) { if (!StartsWithASCII(request.relative_url, "/set-cookie?", true)) return scoped_ptr<net::test_server::HttpResponse>(); scoped_ptr<net::test_server::BasicHttpResponse> http_response( new net::test_server::BasicHttpResponse); http_response->set_code(net::HTTP_OK); size_t query_string_pos = request.relative_url.find('?'); std::string cookie_value = request.relative_url.substr(query_string_pos + 1); std::vector<std::string> cookies; base::SplitString(cookie_value, '&', &cookies); for (size_t i = 0; i < cookies.size(); i++) http_response->AddCustomHeader("Set-Cookie", cookies[i]); return http_response.PassAs<net::test_server::HttpResponse>(); } scoped_ptr<net::test_server::HttpResponse> HandleSetHeaderRequest( const net::test_server::HttpRequest& request) { if (!StartsWithASCII(request.relative_url, "/set-header?", true)) return scoped_ptr<net::test_server::HttpResponse>(); size_t query_string_pos = request.relative_url.find('?'); std::string escaped_header = request.relative_url.substr(query_string_pos + 1); std::string header = net::UnescapeURLComponent(escaped_header, net::UnescapeRule::NORMAL | net::UnescapeRule::SPACES | net::UnescapeRule::URL_SPECIAL_CHARS); size_t colon_pos = header.find(':'); if (colon_pos == std::string::npos) return scoped_ptr<net::test_server::HttpResponse>(); std::string header_name = header.substr(0, colon_pos); // Skip space after colon. std::string header_value = header.substr(colon_pos + 2); scoped_ptr<net::test_server::BasicHttpResponse> http_response( new net::test_server::BasicHttpResponse); http_response->set_code(net::HTTP_OK); http_response->AddCustomHeader(header_name, header_value); return http_response.PassAs<net::test_server::HttpResponse>(); } }; // namespace ExtensionApiTest::ExtensionApiTest() { embedded_test_server()->RegisterRequestHandler( base::Bind(&HandleServerRedirectRequest)); embedded_test_server()->RegisterRequestHandler( base::Bind(&HandleEchoHeaderRequest)); embedded_test_server()->RegisterRequestHandler( base::Bind(&HandleSetCookieRequest)); embedded_test_server()->RegisterRequestHandler( base::Bind(&HandleSetHeaderRequest)); } ExtensionApiTest::~ExtensionApiTest() {} ExtensionApiTest::ResultCatcher::ResultCatcher() : profile_restriction_(NULL), waiting_(false) { registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_TEST_PASSED, content::NotificationService::AllSources()); registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_TEST_FAILED, content::NotificationService::AllSources()); } ExtensionApiTest::ResultCatcher::~ResultCatcher() { } bool ExtensionApiTest::ResultCatcher::GetNextResult() { // Depending on the tests, multiple results can come in from a single call // to RunMessageLoop(), so we maintain a queue of results and just pull them // off as the test calls this, going to the run loop only when the queue is // empty. if (results_.empty()) { waiting_ = true; content::RunMessageLoop(); waiting_ = false; } if (!results_.empty()) { bool ret = results_.front(); results_.pop_front(); message_ = messages_.front(); messages_.pop_front(); return ret; } NOTREACHED(); return false; } void ExtensionApiTest::ResultCatcher::Observe( int type, const content::NotificationSource& source, const content::NotificationDetails& details) { if (profile_restriction_ && content::Source<Profile>(source).ptr() != profile_restriction_) { return; } switch (type) { case chrome::NOTIFICATION_EXTENSION_TEST_PASSED: VLOG(1) << "Got EXTENSION_TEST_PASSED notification."; results_.push_back(true); messages_.push_back(std::string()); if (waiting_) base::MessageLoopForUI::current()->Quit(); break; case chrome::NOTIFICATION_EXTENSION_TEST_FAILED: VLOG(1) << "Got EXTENSION_TEST_FAILED notification."; results_.push_back(false); messages_.push_back(*(content::Details<std::string>(details).ptr())); if (waiting_) base::MessageLoopForUI::current()->Quit(); break; default: NOTREACHED(); } } void ExtensionApiTest::SetUpInProcessBrowserTestFixture() { DCHECK(!test_config_.get()) << "Previous test did not clear config state."; test_config_.reset(new DictionaryValue()); test_config_->SetString(kTestDataDirectory, net::FilePathToFileURL(test_data_dir_).spec()); test_config_->SetInteger(kTestWebSocketPort, 0); extensions::TestGetConfigFunction::set_test_config_state( test_config_.get()); } void ExtensionApiTest::TearDownInProcessBrowserTestFixture() { extensions::TestGetConfigFunction::set_test_config_state(NULL); test_config_.reset(NULL); } bool ExtensionApiTest::RunExtensionTest(const std::string& extension_name) { return RunExtensionTestImpl( extension_name, std::string(), NULL, kFlagEnableFileAccess); } bool ExtensionApiTest::RunExtensionTestIncognito( const std::string& extension_name) { return RunExtensionTestImpl(extension_name, std::string(), NULL, kFlagEnableIncognito | kFlagEnableFileAccess); } bool ExtensionApiTest::RunExtensionTestIgnoreManifestWarnings( const std::string& extension_name) { return RunExtensionTestImpl( extension_name, std::string(), NULL, kFlagIgnoreManifestWarnings); } bool ExtensionApiTest::RunExtensionTestAllowOldManifestVersion( const std::string& extension_name) { return RunExtensionTestImpl( extension_name, std::string(), NULL, kFlagEnableFileAccess | kFlagAllowOldManifestVersions); } bool ExtensionApiTest::RunComponentExtensionTest( const std::string& extension_name) { return RunExtensionTestImpl(extension_name, std::string(), NULL, kFlagEnableFileAccess | kFlagLoadAsComponent); } bool ExtensionApiTest::RunExtensionTestNoFileAccess( const std::string& extension_name) { return RunExtensionTestImpl(extension_name, std::string(), NULL, kFlagNone); } bool ExtensionApiTest::RunExtensionTestIncognitoNoFileAccess( const std::string& extension_name) { return RunExtensionTestImpl( extension_name, std::string(), NULL, kFlagEnableIncognito); } bool ExtensionApiTest::RunExtensionSubtest(const std::string& extension_name, const std::string& page_url) { return RunExtensionSubtest(extension_name, page_url, kFlagEnableFileAccess); } bool ExtensionApiTest::RunExtensionSubtest(const std::string& extension_name, const std::string& page_url, int flags) { DCHECK(!page_url.empty()) << "Argument page_url is required."; // See http://crbug.com/177163 for details. #if defined(OS_WIN) && !defined(NDEBUG) LOG(WARNING) << "Workaround for 177163, prematurely returning"; return true; #endif return RunExtensionTestImpl(extension_name, page_url, NULL, flags); } bool ExtensionApiTest::RunPageTest(const std::string& page_url) { return RunExtensionSubtest(std::string(), page_url); } bool ExtensionApiTest::RunPageTest(const std::string& page_url, int flags) { return RunExtensionSubtest(std::string(), page_url, flags); } bool ExtensionApiTest::RunPlatformAppTest(const std::string& extension_name) { return RunExtensionTestImpl( extension_name, std::string(), NULL, kFlagLaunchPlatformApp); } bool ExtensionApiTest::RunPlatformAppTestWithArg( const std::string& extension_name, const char* custom_arg) { return RunExtensionTestImpl( extension_name, std::string(), custom_arg, kFlagLaunchPlatformApp); } bool ExtensionApiTest::RunPlatformAppTestWithFlags( const std::string& extension_name, int flags) { return RunExtensionTestImpl( extension_name, std::string(), NULL, flags | kFlagLaunchPlatformApp); } // Load |extension_name| extension and/or |page_url| and wait for // PASSED or FAILED notification. bool ExtensionApiTest::RunExtensionTestImpl(const std::string& extension_name, const std::string& page_url, const char* custom_arg, int flags) { bool load_as_component = (flags & kFlagLoadAsComponent) != 0; bool launch_platform_app = (flags & kFlagLaunchPlatformApp) != 0; bool use_incognito = (flags & kFlagUseIncognito) != 0; if (custom_arg && custom_arg[0]) test_config_->SetString(kTestCustomArg, custom_arg); ResultCatcher catcher; DCHECK(!extension_name.empty() || !page_url.empty()) << "extension_name and page_url cannot both be empty"; const extensions::Extension* extension = NULL; if (!extension_name.empty()) { base::FilePath extension_path = test_data_dir_.AppendASCII(extension_name); if (load_as_component) { extension = LoadExtensionAsComponent(extension_path); } else { int browser_test_flags = ExtensionBrowserTest::kFlagNone; if (flags & kFlagEnableIncognito) browser_test_flags |= ExtensionBrowserTest::kFlagEnableIncognito; if (flags & kFlagEnableFileAccess) browser_test_flags |= ExtensionBrowserTest::kFlagEnableFileAccess; if (flags & kFlagIgnoreManifestWarnings) browser_test_flags |= ExtensionBrowserTest::kFlagIgnoreManifestWarnings; if (flags & kFlagAllowOldManifestVersions) { browser_test_flags |= ExtensionBrowserTest::kFlagAllowOldManifestVersions; } extension = LoadExtensionWithFlags(extension_path, browser_test_flags); } if (!extension) { message_ = "Failed to load extension."; return false; } } // If there is a page_url to load, navigate it. if (!page_url.empty()) { GURL url = GURL(page_url); // Note: We use is_valid() here in the expectation that the provided url // may lack a scheme & host and thus be a relative url within the loaded // extension. if (!url.is_valid()) { DCHECK(!extension_name.empty()) << "Relative page_url given with no extension_name"; url = extension->GetResourceURL(page_url); } if (use_incognito) ui_test_utils::OpenURLOffTheRecord(browser()->profile(), url); else ui_test_utils::NavigateToURL(browser(), url); } else if (launch_platform_app) { AppLaunchParams params(browser()->profile(), extension, extensions::LAUNCH_CONTAINER_NONE, NEW_WINDOW); params.command_line = CommandLine::ForCurrentProcess(); OpenApplication(params); } if (!catcher.GetNextResult()) { message_ = catcher.message(); return false; } return true; } // Test that exactly one extension is loaded, and return it. const extensions::Extension* ExtensionApiTest::GetSingleLoadedExtension() { ExtensionService* service = extensions::ExtensionSystem::Get( browser()->profile())->extension_service(); const extensions::Extension* extension = NULL; for (ExtensionSet::const_iterator it = service->extensions()->begin(); it != service->extensions()->end(); ++it) { // Ignore any component extensions. They are automatically loaded into all // profiles and aren't the extension we're looking for here. if ((*it)->location() == extensions::Manifest::COMPONENT) continue; if (extension != NULL) { // TODO(yoz): this is misleading; it counts component extensions. message_ = base::StringPrintf( "Expected only one extension to be present. Found %u.", static_cast<unsigned>(service->extensions()->size())); return NULL; } extension = it->get(); } if (!extension) { message_ = "extension pointer is NULL."; return NULL; } return extension; } bool ExtensionApiTest::StartEmbeddedTestServer() { if (!embedded_test_server()->InitializeAndWaitUntilReady()) return false; // Build a dictionary of values that tests can use to build URLs that // access the test server and local file system. Tests can see these values // using the extension API function chrome.test.getConfig(). test_config_->SetInteger(kTestServerPort, embedded_test_server()->port()); return true; } bool ExtensionApiTest::StartWebSocketServer( const base::FilePath& root_directory) { websocket_server_.reset(new net::SpawnedTestServer( net::SpawnedTestServer::TYPE_WS, net::SpawnedTestServer::kLocalhost, root_directory)); if (!websocket_server_->Start()) return false; test_config_->SetInteger(kTestWebSocketPort, websocket_server_->host_port_pair().port()); return true; } bool ExtensionApiTest::StartSpawnedTestServer() { if (!test_server()->Start()) return false; // Build a dictionary of values that tests can use to build URLs that // access the test server and local file system. Tests can see these values // using the extension API function chrome.test.getConfig(). test_config_->SetInteger(kSpawnedTestServerPort, test_server()->host_port_pair().port()); return true; } void ExtensionApiTest::SetUpCommandLine(CommandLine* command_line) { ExtensionBrowserTest::SetUpCommandLine(command_line); test_data_dir_ = test_data_dir_.AppendASCII("api_test"); }