Index: extensions/browser/content_verify_job_unittest.cc |
diff --git a/extensions/browser/content_verify_job_unittest.cc b/extensions/browser/content_verify_job_unittest.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..8466e2f68f37ee3435c9810aa0664e63dc2f8fd2 |
--- /dev/null |
+++ b/extensions/browser/content_verify_job_unittest.cc |
@@ -0,0 +1,252 @@ |
+// Copyright 2017 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 "base/files/file_path.h" |
+#include "base/files/file_util.h" |
+#include "base/files/scoped_temp_dir.h" |
+#include "base/memory/ptr_util.h" |
+#include "base/path_service.h" |
+#include "base/run_loop.h" |
+#include "base/version.h" |
+#include "content/public/browser/browser_thread.h" |
+#include "content/public/test/test_browser_thread_bundle.h" |
+#include "extensions/browser/content_hash_reader.h" |
+#include "extensions/browser/extensions_test.h" |
+#include "extensions/common/constants.h" |
+#include "extensions/common/extension_paths.h" |
+#include "extensions/common/file_util.h" |
+#include "testing/gtest/include/gtest/gtest.h" |
+#include "third_party/zlib/google/zip.h" |
+ |
+namespace extensions { |
+ |
+namespace { |
+ |
+scoped_refptr<ContentHashReader> CreateContentHashReader( |
+ const Extension& extension, |
+ base::FilePath& extension_resource_path) { |
+ return make_scoped_refptr(new ContentHashReader( |
+ extension.id(), *extension.version(), extension.path(), |
+ extension_resource_path, |
+ ContentVerifierKey(kWebstoreSignaturesPublicKey, |
+ kWebstoreSignaturesPublicKeySize))); |
+} |
+ |
+void DoNothingWithReasonParam(ContentVerifyJob::FailureReason reason) {} |
+ |
+class JobTestObserver : public ContentVerifyJob::TestObserver { |
+ public: |
+ JobTestObserver(const std::string& extension_id, |
+ const base::FilePath& relative_path) |
+ : extension_id_(extension_id), relative_path_(relative_path) { |
+ ContentVerifyJob::SetObserverForTests(this); |
+ } |
+ ~JobTestObserver() { ContentVerifyJob::SetObserverForTests(nullptr); } |
+ |
+ void JobStarted(const std::string& extension_id, |
+ const base::FilePath& relative_path) override {} |
+ |
+ void JobFinished(const std::string& extension_id, |
+ const base::FilePath& relative_path, |
+ ContentVerifyJob::FailureReason reason) override { |
+ if (extension_id != extension_id_ || relative_path != relative_path_) |
+ return; |
+ failure_reason_ = reason; |
+ run_loop_.Quit(); |
+ } |
+ |
+ ContentVerifyJob::FailureReason WaitAndGetFailureReason() { |
+ // Run() returns immediately if Quit() has already been called. |
+ run_loop_.Run(); |
+ EXPECT_TRUE(failure_reason_.has_value()); |
+ return failure_reason_.value_or(ContentVerifyJob::FAILURE_REASON_MAX); |
+ } |
+ |
+ private: |
+ base::RunLoop run_loop_; |
+ std::string extension_id_; |
+ base::FilePath relative_path_; |
+ base::Optional<ContentVerifyJob::FailureReason> failure_reason_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(JobTestObserver); |
+}; |
+ |
+} // namespace |
+ |
+class ContentVerifyJobUnittest : public ExtensionsTest { |
+ public: |
+ ContentVerifyJobUnittest() {} |
+ ~ContentVerifyJobUnittest() override {} |
+ |
+ void SetUp() override { |
+ ExtensionsTest::SetUp(); |
+ |
+ // Needed for ContentVerifyJob::Start(). |
+ browser_threads_ = base::MakeUnique<content::TestBrowserThreadBundle>( |
+ content::TestBrowserThreadBundle::REAL_IO_THREAD); |
+ } |
+ |
+ // Helper to get files from our subdirectory in the general extensions test |
+ // data dir. |
+ base::FilePath GetTestPath(const base::FilePath& relative_path) { |
+ base::FilePath base_path; |
+ EXPECT_TRUE(PathService::Get(DIR_TEST_DATA, &base_path)); |
+ base_path = base_path.AppendASCII("content_hash_fetcher"); |
+ return base_path.Append(relative_path); |
+ } |
+ |
+ // Unzips the extension source from |extension_zip| into a temporary |
+ // directory and loads it. Returns the resuling Extension object. |
+ // |destination| points to the path where the extension was extracted. |
+ scoped_refptr<Extension> UnzipToTempDirAndLoad( |
+ const base::FilePath& extension_zip, |
+ base::FilePath* destination) { |
+ EXPECT_TRUE(temp_dir_.CreateUniqueTempDir()); |
+ *destination = temp_dir_.GetPath(); |
+ EXPECT_TRUE(zip::Unzip(extension_zip, *destination)); |
+ |
+ std::string error; |
+ scoped_refptr<Extension> extension = file_util::LoadExtension( |
+ *destination, Manifest::INTERNAL, 0 /* flags */, &error); |
+ EXPECT_NE(nullptr, extension.get()) << " error:'" << error << "'"; |
+ return extension; |
+ } |
+ |
+ private: |
+ base::ScopedTempDir temp_dir_; |
+ std::unique_ptr<content::TestBrowserThreadBundle> browser_threads_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(ContentVerifyJobUnittest); |
+}; |
+ |
+// Tests that deleted legitimate files trigger content verification failure. |
+// Also tests that non-existent file request does not trigger content |
+// verification failure. |
+TEST_F(ContentVerifyJobUnittest, DeletedAndMissingFiles) { |
+ base::FilePath unzipped_path; |
+ base::FilePath test_dir_base = |
+ GetTestPath(base::FilePath(FILE_PATH_LITERAL("with_verified_contents"))); |
+ scoped_refptr<Extension> extension = UnzipToTempDirAndLoad( |
+ test_dir_base.AppendASCII("source_all.zip"), &unzipped_path); |
+ ASSERT_TRUE(extension.get()); |
+ // Make sure there is a verified_contents.json file there as this test cannot |
+ // fetch it. |
+ EXPECT_TRUE( |
+ base::PathExists(file_util::GetVerifiedContentsPath(extension->path()))); |
+ |
+ const base::FilePath::CharType kExistentResource[] = |
+ FILE_PATH_LITERAL("background.js"); |
+ base::FilePath existent_resource_path(kExistentResource); |
+ { |
+ // Make sure background.js passes verification correctly. |
+ JobTestObserver observer(extension->id(), existent_resource_path); |
+ |
+ scoped_refptr<ContentHashReader> content_hash_reader = |
+ CreateContentHashReader(*extension.get(), existent_resource_path); |
+ scoped_refptr<ContentVerifyJob> verify_job = new ContentVerifyJob( |
+ content_hash_reader.get(), base::Bind(&DoNothingWithReasonParam)); |
+ verify_job->Start(); |
+ { |
+ // Simulate serving background.js. |
+ std::string background_contents; |
+ base::ReadFileToString( |
+ unzipped_path.Append(base::FilePath(kExistentResource)), |
+ &background_contents); |
+ verify_job->BytesRead(background_contents.size(), |
+ base::string_as_array(&background_contents)); |
+ verify_job->DoneReading(); |
+ } |
+ ContentVerifyJob::FailureReason reason = observer.WaitAndGetFailureReason(); |
+ // Expect no content-verification failure. |
+ EXPECT_EQ(ContentVerifyJob::NONE, reason); |
+ } |
+ |
+ { |
+ // Once background.js is deleted, verification will result in HASH_MISMATCH. |
+ JobTestObserver observer(extension->id(), existent_resource_path); |
+ // Now delete the existent file. |
+ EXPECT_TRUE(base::DeleteFile( |
+ unzipped_path.Append(base::FilePath(kExistentResource)), false)); |
+ |
+ scoped_refptr<ContentHashReader> content_hash_reader = |
+ CreateContentHashReader(*extension.get(), existent_resource_path); |
+ scoped_refptr<ContentVerifyJob> verify_job = new ContentVerifyJob( |
+ content_hash_reader.get(), base::Bind(&DoNothingWithReasonParam)); |
+ verify_job->Start(); |
+ { |
+ // Simulate serving deleted background.js. |
+ std::string tmp; |
+ verify_job->BytesRead(0, base::string_as_array(&tmp)); |
+ verify_job->DoneReading(); |
+ } |
+ ContentVerifyJob::FailureReason reason = observer.WaitAndGetFailureReason(); |
+ EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH, reason); |
+ } |
+ |
+ { |
+ // Now ask for a non-existent resource non-existent.js. Verification should |
+ // skip this file as it is not listed in our verified_contents.json file. |
+ const base::FilePath::CharType kNonExistentResource[] = |
+ FILE_PATH_LITERAL("non-existent.js"); |
+ base::FilePath non_existent_resource_path(kNonExistentResource); |
+ JobTestObserver observer(extension->id(), non_existent_resource_path); |
+ |
+ scoped_refptr<ContentHashReader> content_hash_reader = |
+ CreateContentHashReader(*extension.get(), non_existent_resource_path); |
+ scoped_refptr<ContentVerifyJob> verify_job = new ContentVerifyJob( |
+ content_hash_reader.get(), base::Bind(&DoNothingWithReasonParam)); |
+ verify_job->Start(); |
+ { |
+ // Simulate non existent file read. |
+ std::string tmp; |
+ verify_job->BytesRead(0, base::string_as_array(&tmp)); |
+ verify_job->DoneReading(); |
+ } |
+ ContentVerifyJob::FailureReason reason = observer.WaitAndGetFailureReason(); |
+ // Expect no content-verification failure. |
+ EXPECT_EQ(ContentVerifyJob::NONE, reason); |
+ } |
+} |
+ |
+// Tests that content modification causes content verification failure. |
+TEST_F(ContentVerifyJobUnittest, ContentMismatch) { |
+ base::FilePath unzipped_path; |
+ base::FilePath test_dir_base = |
+ GetTestPath(base::FilePath(FILE_PATH_LITERAL("with_verified_contents"))); |
+ scoped_refptr<Extension> extension = UnzipToTempDirAndLoad( |
+ test_dir_base.AppendASCII("source_all.zip"), &unzipped_path); |
+ ASSERT_TRUE(extension.get()); |
+ // Make sure there is a verified_contents.json file there as this test cannot |
+ // fetch it. |
+ EXPECT_TRUE( |
+ base::PathExists(file_util::GetVerifiedContentsPath(extension->path()))); |
+ |
+ const base::FilePath::CharType kResource[] = |
+ FILE_PATH_LITERAL("background.js"); |
+ base::FilePath existent_resource_path(kResource); |
+ { |
+ // Make sure modified background.js fails content verification. |
+ JobTestObserver observer(extension->id(), existent_resource_path); |
+ |
+ scoped_refptr<ContentHashReader> content_hash_reader = |
+ CreateContentHashReader(*extension.get(), existent_resource_path); |
+ scoped_refptr<ContentVerifyJob> verify_job = new ContentVerifyJob( |
+ content_hash_reader.get(), base::Bind(&DoNothingWithReasonParam)); |
+ verify_job->Start(); |
+ { |
+ // Simulate serving *modified* background.js. |
+ std::string modified_contents; |
+ base::ReadFileToString(unzipped_path.Append(base::FilePath(kResource)), |
+ &modified_contents); |
+ modified_contents.append("console.log('modified');"); |
+ verify_job->BytesRead(modified_contents.size(), |
+ base::string_as_array(&modified_contents)); |
+ verify_job->DoneReading(); |
+ } |
+ ContentVerifyJob::FailureReason reason = observer.WaitAndGetFailureReason(); |
+ EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH, reason); |
+ } |
+} |
+ |
+} // namespace extensions |