blob: 8458740cc73466dcf38fbe745c14aa28af7c7891 [file] [log] [blame]
// Copyright 2019 The Chromium OS 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 "arc/apk-cache/cache_cleaner_db.h"
#include <base/bind.h>
#include <base/files/file_enumerator.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/files/scoped_temp_dir.h>
#include <base/optional.h>
#include <base/time/time.h>
#include <gtest/gtest.h>
#include <sqlite3.h>
#include "arc/apk-cache/apk_cache_database.h"
#include "arc/apk-cache/apk_cache_database_test_utils.h"
#include "arc/apk-cache/apk_cache_utils.h"
namespace apk_cache {
namespace {
constexpr char kBrokenDatabaseContent[] = "test broken db file content";
constexpr char kTestSessionSource[] = "unit_test_session_source";
constexpr int64_t kTestSessionId = 123;
constexpr int64_t kTestBaseApkId = 1234;
constexpr int64_t kTestAttachmentId = 1235;
constexpr char kTestPackageName[] = "com.package.test";
constexpr int64_t kTestVersionCode = 1234;
constexpr char kTestAttachmentType[] = "test.foo.bar";
constexpr char kTestFileContent[] = "test file content";
constexpr char kTestFileHash[] = "2Q7xZR_Z51Y-GhRQoWvXhOmn4tPfD1p5jfwb33CmSuo";
constexpr int32_t kTestPackagePriority = 100;
bool CreateSession(const base::FilePath& db_path, int64_t id, int32_t status) {
Session session;
session.id = id;
session.source = kTestSessionSource;
session.timestamp = base::Time::Now();
session.status = status;
return InsertSessionForTesting(db_path, session);
}
bool CreateFileEntry(const base::FilePath& db_path,
const base::FilePath& files_path,
int64_t id,
const std::string& package_name,
int64_t version_code,
const std::string& type) {
FileEntry file_entry;
file_entry.id = id;
file_entry.package_name = package_name;
file_entry.version_code = version_code;
file_entry.type = type;
file_entry.size = strlen(kTestFileContent);
file_entry.hash = std::string(kTestFileHash);
file_entry.access_time = base::Time::Now();
file_entry.priority = kTestPackagePriority;
file_entry.session_id = kTestSessionId;
if (!InsertFileEntryForTesting(db_path, file_entry))
return false;
base::FilePath file_path = files_path.Append(GetFileNameById(id));
return base::WriteFile(file_path, kTestFileContent, strlen(kTestFileContent));
}
bool CreateValidPackage(const base::FilePath& db_path,
const base::FilePath& files_path) {
return CreateSession(db_path, kTestSessionId, kSessionStatusClosed) &&
CreateFileEntry(db_path, files_path, kTestBaseApkId, kTestPackageName,
kTestVersionCode, kFileTypeBaseApk) &&
CreateFileEntry(db_path, files_path, kTestAttachmentId,
kTestPackageName, kTestVersionCode,
kTestAttachmentType);
}
} // namespace
class CacheCleanerDBTest : public testing::Test {
public:
const base::FilePath& temp_path() const { return temp_dir_.GetPath(); }
protected:
CacheCleanerDBTest() = default;
// Not copyable or movable.
CacheCleanerDBTest(const CacheCleanerDBTest&) = delete;
CacheCleanerDBTest& operator=(const CacheCleanerDBTest&) = delete;
void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
void TearDown() override { ASSERT_TRUE(temp_dir_.Delete()); }
private:
base::ScopedTempDir temp_dir_;
};
// If database does not exist, files directory should be removed.
TEST_F(CacheCleanerDBTest, DatabaseNotExist) {
// Create files directory.
base::FilePath files_path = temp_path().Append(kFilesBase);
ASSERT_TRUE(base::CreateDirectory(files_path));
// Write a random file to files directory.
base::FilePath file_path = files_path.Append("test");
ASSERT_TRUE(
base::WriteFile(file_path, kTestFileContent, strlen(kTestFileContent)));
// Clean.
EXPECT_TRUE(OpaqueFilesCleaner(temp_path()).Clean());
// Files directory should be removed.
EXPECT_FALSE(base::PathExists(files_path));
}
// If database is empty, files directory should be removed.
TEST_F(CacheCleanerDBTest, EmptyDatabase) {
// Write empty database file.
base::FilePath db_path = temp_path().Append(kDatabaseFile);
ASSERT_TRUE(base::WriteFile(db_path, "\0", 1));
ASSERT_TRUE(base::PathExists(db_path));
// Create files directory.
base::FilePath files_path = temp_path().Append(kFilesBase);
ASSERT_TRUE(base::CreateDirectory(files_path));
// Write a random file to files directory.
base::FilePath file_path = files_path.Append("test");
ASSERT_TRUE(
base::WriteFile(file_path, kTestFileContent, strlen(kTestFileContent)));
// Clean.
EXPECT_TRUE(OpaqueFilesCleaner(temp_path()).Clean());
// Files directory should be removed.
EXPECT_FALSE(base::PathExists(files_path));
}
// Correct database structure should pass integrity test.
TEST_F(CacheCleanerDBTest, ApkCacheDatabase) {
base::FilePath db_path = temp_path().Append(kDatabaseFile);
ASSERT_EQ(CreateDatabaseForTesting(db_path), SQLITE_OK);
EXPECT_TRUE(base::PathExists(db_path));
EXPECT_TRUE(OpaqueFilesCleaner(temp_path()).Clean());
}
// If database file is broken, it should be removed with files directory.
TEST_F(CacheCleanerDBTest, BrokenDatabaseFile) {
// Write random content to database file.
base::FilePath db_path = temp_path().Append(kDatabaseFile);
base::WriteFile(db_path, kBrokenDatabaseContent,
strlen(kBrokenDatabaseContent));
EXPECT_TRUE(base::PathExists(db_path));
// Create files directory.
base::FilePath files_path = temp_path().Append(kFilesBase);
ASSERT_TRUE(base::CreateDirectory(files_path));
// Write a random file to files directory.
base::FilePath file_path = files_path.Append("test");
ASSERT_TRUE(
base::WriteFile(file_path, kTestFileContent, strlen(kTestFileContent)));
// Clean.
EXPECT_TRUE(OpaqueFilesCleaner(temp_path()).Clean());
// Both database file and files directory should be removed.
EXPECT_FALSE(base::PathExists(db_path));
EXPECT_FALSE(base::PathExists(files_path));
}
// Cache cleaner should create a session before cleaning.
TEST_F(CacheCleanerDBTest, CacheCleanerSession) {
base::FilePath db_path = temp_path().Append(kDatabaseFile);
ASSERT_EQ(CreateDatabaseForTesting(db_path), SQLITE_OK);
EXPECT_TRUE(base::PathExists(db_path));
// Clean.
OpaqueFilesCleaner cleaner(temp_path());
EXPECT_TRUE(cleaner.Clean());
// Cache cleaner session should be created.
ApkCacheDatabase db(db_path);
ASSERT_EQ(db.Init(), SQLITE_OK);
auto sessions = db.GetSessions();
ASSERT_TRUE(sessions);
ASSERT_EQ(sessions->size(), 1);
ASSERT_EQ((*sessions)[0].source, kCacheCleanerSessionSource);
}
// If an open session is not expired, cleaner should exit.
TEST_F(CacheCleanerDBTest, OtherSessionActive) {
base::FilePath db_path = temp_path().Append(kDatabaseFile);
ASSERT_EQ(CreateDatabaseForTesting(db_path), SQLITE_OK);
EXPECT_TRUE(base::PathExists(db_path));
// Create an open session.
ASSERT_TRUE(CreateSession(db_path, kTestSessionId, kSessionStatusOpen));
// Clean.
OpaqueFilesCleaner cleaner(temp_path());
EXPECT_TRUE(cleaner.Clean());
// Cache cleaner session should not be created.
ApkCacheDatabase db(db_path);
ASSERT_EQ(db.Init(), SQLITE_OK);
auto sessions = db.GetSessions();
ASSERT_TRUE(sessions);
for (Session session : *sessions)
EXPECT_NE(session.source, kCacheCleanerSessionSource);
}
// Session without file entries should be removed.
TEST_F(CacheCleanerDBTest, SessionWithoutFileEntries) {
base::FilePath db_path = temp_path().Append(kDatabaseFile);
ASSERT_EQ(CreateDatabaseForTesting(db_path), SQLITE_OK);
EXPECT_TRUE(base::PathExists(db_path));
// Create session.
CreateSession(db_path, kTestSessionId, kSessionStatusClosed);
// Clean.
EXPECT_TRUE(OpaqueFilesCleaner(temp_path()).Clean());
// Test session should be removed.
ApkCacheDatabase db(db_path);
ASSERT_EQ(db.Init(), SQLITE_OK);
auto sessions = db.GetSessions();
ASSERT_TRUE(sessions);
bool session_exists = false;
for (Session session : *sessions)
if (session.id == kTestSessionId) {
session_exists = true;
break;
}
EXPECT_FALSE(session_exists);
}
// Expired open sessions should be removed.
TEST_F(CacheCleanerDBTest, ExpiredOpenSessions) {
base::FilePath db_path = temp_path().Append(kDatabaseFile);
ASSERT_EQ(CreateDatabaseForTesting(db_path), SQLITE_OK);
EXPECT_TRUE(base::PathExists(db_path));
// Create valid package.
base::FilePath files_path = temp_path().Append(kFilesBase);
ASSERT_TRUE(base::CreateDirectory(files_path));
EXPECT_TRUE(CreateValidPackage(db_path, files_path));
// Change session status to open.
UpdateSessionStatusForTesting(db_path, kTestSessionId, kSessionStatusOpen);
// Let session expire.
UpdateSessionTimestampForTesting(
db_path, kTestSessionId,
base::Time::Now() - kSessionMaxAge - base::TimeDelta::FromSeconds(1));
// Clean.
EXPECT_TRUE(OpaqueFilesCleaner(temp_path()).Clean());
// Test session should be removed.
ApkCacheDatabase db(db_path);
ASSERT_EQ(db.Init(), SQLITE_OK);
auto sessions = db.GetSessions();
ASSERT_TRUE(sessions);
bool session_exists = false;
for (Session session : *sessions)
if (session.id == kTestSessionId) {
session_exists = true;
break;
}
EXPECT_FALSE(session_exists);
// Package should be removed.
auto file_entries = db.GetFileEntries();
ASSERT_TRUE(file_entries);
EXPECT_EQ(file_entries->size(), 0);
}
// Valid package should not be removed.
TEST_F(CacheCleanerDBTest, ValidPackage) {
base::FilePath db_path = temp_path().Append(kDatabaseFile);
ASSERT_EQ(CreateDatabaseForTesting(db_path), SQLITE_OK);
EXPECT_TRUE(base::PathExists(db_path));
// Create valid package.
base::FilePath files_path = temp_path().Append(kFilesBase);
ASSERT_TRUE(base::CreateDirectory(files_path));
EXPECT_TRUE(CreateValidPackage(db_path, files_path));
// Clean.
EXPECT_TRUE(OpaqueFilesCleaner(temp_path()).Clean());
// Package should still exist.
ApkCacheDatabase db(db_path);
ASSERT_EQ(db.Init(), SQLITE_OK);
auto file_entries = db.GetFileEntries();
ASSERT_TRUE(file_entries);
EXPECT_EQ(file_entries->size(), 2);
}
// If a file is expired, the whole package should be removed.
TEST_F(CacheCleanerDBTest, ExpiredFile) {
base::FilePath db_path = temp_path().Append(kDatabaseFile);
ASSERT_EQ(CreateDatabaseForTesting(db_path), SQLITE_OK);
EXPECT_TRUE(base::PathExists(db_path));
// Create valid package.
base::FilePath files_path = temp_path().Append(kFilesBase);
ASSERT_TRUE(base::CreateDirectory(files_path));
EXPECT_TRUE(CreateValidPackage(db_path, files_path));
// Update timestamp so that base APK is expired.
base::Time access_time =
base::Time::Now() - kValidityPeriod - base::TimeDelta::FromSeconds(1);
ASSERT_TRUE(
UpdateFileAccessTimeForTesting(db_path, kTestBaseApkId, access_time));
// Clean.
EXPECT_TRUE(OpaqueFilesCleaner(temp_path()).Clean());
// Package should be removed.
ApkCacheDatabase db(db_path);
ASSERT_EQ(db.Init(), SQLITE_OK);
auto file_entries = db.GetFileEntries();
ASSERT_TRUE(file_entries);
EXPECT_EQ(file_entries->size(), 0);
}
// If a file does not have a record in database, it should be removed.
TEST_F(CacheCleanerDBTest, FileWithoutRecord) {
base::FilePath db_path = temp_path().Append(kDatabaseFile);
ASSERT_EQ(CreateDatabaseForTesting(db_path), SQLITE_OK);
EXPECT_TRUE(base::PathExists(db_path));
// Create valid package.
base::FilePath files_path = temp_path().Append(kFilesBase);
ASSERT_TRUE(base::CreateDirectory(files_path));
EXPECT_TRUE(CreateValidPackage(db_path, files_path));
// Create a random file in files directory.
base::FilePath random_file = files_path.Append("foobar");
base::WriteFile(random_file, kTestFileContent, strlen(kTestFileContent));
// The file should exist.
EXPECT_TRUE(base::PathExists(random_file));
// Clean.
EXPECT_TRUE(OpaqueFilesCleaner(temp_path()).Clean());
// The file should be removed.
EXPECT_FALSE(base::PathExists(random_file));
}
// If a directory appears in files directory, it should be removed.
TEST_F(CacheCleanerDBTest, DirectoryInFiles) {
base::FilePath db_path = temp_path().Append(kDatabaseFile);
ASSERT_EQ(CreateDatabaseForTesting(db_path), SQLITE_OK);
EXPECT_TRUE(base::PathExists(db_path));
// Create valid package.
base::FilePath files_path = temp_path().Append(kFilesBase);
ASSERT_TRUE(base::CreateDirectory(files_path));
EXPECT_TRUE(CreateValidPackage(db_path, files_path));
// Create a random directory in files directory.
base::FilePath random_dir = files_path.Append("foobar");
base::CreateDirectory(random_dir);
base::FilePath random_file = random_dir.Append("test");
ASSERT_TRUE(
base::WriteFile(random_file, kTestFileContent, strlen(kTestFileContent)));
// The directory should exist.
EXPECT_TRUE(base::PathExists(random_dir));
EXPECT_TRUE(base::PathExists(random_file));
// Clean.
EXPECT_TRUE(OpaqueFilesCleaner(temp_path()).Clean());
// The directory should be removed.
EXPECT_FALSE(base::PathExists(random_dir));
EXPECT_FALSE(base::PathExists(random_file));
}
} // namespace apk_cache