2016-02-11 16:39:42 +01:00
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <cryfs/config/CryConfigLoader.h>
|
2015-10-23 12:16:23 +02:00
|
|
|
#include "../testutils/MockConsole.h"
|
2017-09-30 10:03:19 +02:00
|
|
|
#include "../testutils/TestWithFakeHomeDirectory.h"
|
2016-02-11 16:39:42 +01:00
|
|
|
#include <cpp-utils/tempfile/TempFile.h>
|
|
|
|
#include <cpp-utils/random/Random.h>
|
|
|
|
#include <cpp-utils/crypto/symmetric/ciphers.h>
|
2016-06-21 01:14:07 +02:00
|
|
|
#include <cpp-utils/data/DataFixture.h>
|
2016-09-25 02:50:28 +02:00
|
|
|
#include <cpp-utils/io/NoninteractiveConsole.h>
|
2016-03-26 17:09:07 +01:00
|
|
|
#include <gitversion/gitversion.h>
|
2016-05-04 05:34:30 +02:00
|
|
|
#include <gitversion/VersionCompare.h>
|
2015-10-23 12:16:23 +02:00
|
|
|
|
|
|
|
using cpputils::unique_ref;
|
|
|
|
using cpputils::make_unique_ref;
|
|
|
|
using cpputils::TempFile;
|
2015-11-04 05:27:00 +01:00
|
|
|
using cpputils::SCrypt;
|
2016-06-21 01:14:07 +02:00
|
|
|
using cpputils::DataFixture;
|
2017-09-30 10:30:31 +02:00
|
|
|
using cpputils::Data;
|
2016-09-25 02:50:28 +02:00
|
|
|
using cpputils::NoninteractiveConsole;
|
2015-10-24 19:35:37 +02:00
|
|
|
using boost::optional;
|
|
|
|
using boost::none;
|
2015-10-23 12:16:23 +02:00
|
|
|
using std::string;
|
2015-11-24 14:42:20 +01:00
|
|
|
using std::ostream;
|
2016-09-25 02:50:28 +02:00
|
|
|
using std::make_shared;
|
2015-10-23 12:16:23 +02:00
|
|
|
using ::testing::Return;
|
|
|
|
using ::testing::_;
|
2016-05-04 05:34:30 +02:00
|
|
|
using ::testing::HasSubstr;
|
2015-10-23 12:16:23 +02:00
|
|
|
|
|
|
|
using namespace cryfs;
|
|
|
|
|
2015-11-24 14:42:20 +01:00
|
|
|
// This is needed for google test
|
|
|
|
namespace boost {
|
|
|
|
inline ostream &operator<<(ostream &stream, const CryConfigFile &) {
|
|
|
|
return stream << "CryConfigFile()";
|
|
|
|
}
|
|
|
|
}
|
2017-10-02 09:01:38 +02:00
|
|
|
namespace cryfs {
|
|
|
|
inline ostream &operator<<(ostream &stream, const CryConfigLoader::ConfigLoadResult &) {
|
|
|
|
return stream << "ConfigLoadResult()";
|
|
|
|
}
|
|
|
|
}
|
2016-02-13 20:42:28 +01:00
|
|
|
#include <boost/optional/optional_io.hpp>
|
2015-11-24 14:42:20 +01:00
|
|
|
|
2017-09-30 10:30:31 +02:00
|
|
|
class FakeRandomGenerator final : public cpputils::RandomGenerator {
|
|
|
|
public:
|
|
|
|
FakeRandomGenerator(Data output)
|
|
|
|
: _output(std::move(output)) {}
|
|
|
|
|
|
|
|
void _get(void *target, size_t bytes) override {
|
|
|
|
ASSERT_EQ(_output.size(), bytes);
|
|
|
|
std::memcpy(target, _output.data(), bytes);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
Data _output;
|
|
|
|
};
|
|
|
|
|
2017-09-30 10:03:19 +02:00
|
|
|
class CryConfigLoaderTest: public ::testing::Test, public TestWithMockConsole, TestWithFakeHomeDirectory {
|
2015-10-23 12:16:23 +02:00
|
|
|
public:
|
2016-05-04 05:34:30 +02:00
|
|
|
CryConfigLoaderTest(): file(false) {
|
|
|
|
console = mockConsole();
|
|
|
|
}
|
2015-10-23 12:16:23 +02:00
|
|
|
|
2016-02-21 01:34:21 +01:00
|
|
|
CryConfigLoader loader(const string &password, bool noninteractive, const optional<string> &cipher = none) {
|
|
|
|
auto askPassword = [password] { return password;};
|
2016-09-25 02:50:28 +02:00
|
|
|
if(noninteractive) {
|
|
|
|
return CryConfigLoader(make_shared<NoninteractiveConsole>(console), cpputils::Random::PseudoRandom(), SCrypt::TestSettings, askPassword,
|
2016-09-25 02:53:35 +02:00
|
|
|
askPassword, cipher, none, none);
|
2016-09-25 02:50:28 +02:00
|
|
|
} else {
|
|
|
|
return CryConfigLoader(console, cpputils::Random::PseudoRandom(), SCrypt::TestSettings, askPassword,
|
2016-09-25 02:53:35 +02:00
|
|
|
askPassword, cipher, none, none);
|
2016-09-25 02:50:28 +02:00
|
|
|
}
|
2015-10-24 19:35:37 +02:00
|
|
|
}
|
|
|
|
|
2016-02-21 01:34:21 +01:00
|
|
|
CryConfigFile Create(const string &password = "mypassword", const optional<string> &cipher = none, bool noninteractive = false) {
|
2015-10-23 12:16:23 +02:00
|
|
|
EXPECT_FALSE(file.exists());
|
2016-06-27 01:53:10 +02:00
|
|
|
return loader(password, noninteractive, cipher).loadOrCreate(file.path()).value().configFile;
|
2015-10-23 12:16:23 +02:00
|
|
|
}
|
|
|
|
|
2016-02-21 01:34:21 +01:00
|
|
|
optional<CryConfigFile> Load(const string &password = "mypassword", const optional<string> &cipher = none, bool noninteractive = false) {
|
2015-10-23 12:16:23 +02:00
|
|
|
EXPECT_TRUE(file.exists());
|
2016-06-27 01:53:10 +02:00
|
|
|
auto loadResult = loader(password, noninteractive, cipher).loadOrCreate(file.path());
|
|
|
|
if (loadResult == none) {
|
|
|
|
return none;
|
|
|
|
}
|
|
|
|
return std::move(loadResult->configFile);
|
2015-10-23 12:16:23 +02:00
|
|
|
}
|
|
|
|
|
2015-10-24 19:35:37 +02:00
|
|
|
void CreateWithRootBlob(const string &rootBlob, const string &password = "mypassword") {
|
2016-06-27 01:53:10 +02:00
|
|
|
auto cfg = loader(password, false).loadOrCreate(file.path()).value().configFile;
|
2015-10-23 12:16:23 +02:00
|
|
|
cfg.config()->SetRootBlob(rootBlob);
|
|
|
|
cfg.save();
|
|
|
|
}
|
|
|
|
|
2015-10-24 19:35:37 +02:00
|
|
|
void CreateWithCipher(const string &cipher, const string &password = "mypassword") {
|
2016-06-27 01:53:10 +02:00
|
|
|
auto cfg = loader(password, false).loadOrCreate(file.path()).value().configFile;
|
2015-10-23 12:16:23 +02:00
|
|
|
cfg.config()->SetCipher(cipher);
|
|
|
|
cfg.save();
|
|
|
|
}
|
|
|
|
|
2015-10-24 19:35:37 +02:00
|
|
|
void CreateWithEncryptionKey(const string &encKey, const string &password = "mypassword") {
|
2017-09-30 10:30:31 +02:00
|
|
|
auto askPassword = [password] { return password;};
|
|
|
|
FakeRandomGenerator generator(Data::FromString(encKey));
|
|
|
|
auto loader = CryConfigLoader(console, generator, SCrypt::TestSettings, askPassword,
|
|
|
|
askPassword, none, none, none);
|
2017-10-02 09:01:38 +02:00
|
|
|
ASSERT_NE(boost::none, loader.loadOrCreate(file.path()));
|
2017-09-30 23:24:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void ChangeEncryptionKey(const string &encKey, const string& password = "mypassword") {
|
2017-09-30 23:44:24 +02:00
|
|
|
auto cfg = CryConfigFile::load(file.path(), password).value();
|
2015-10-23 12:16:23 +02:00
|
|
|
cfg.config()->SetEncryptionKey(encKey);
|
|
|
|
cfg.save();
|
|
|
|
}
|
|
|
|
|
2016-03-26 17:09:07 +01:00
|
|
|
void CreateWithVersion(const string &version, const string &password = "mypassword") {
|
2016-06-27 01:53:10 +02:00
|
|
|
auto cfg = loader(password, false).loadOrCreate(file.path()).value().configFile;
|
2016-03-26 17:09:07 +01:00
|
|
|
cfg.config()->SetVersion(version);
|
|
|
|
cfg.config()->SetCreatedWithVersion(version);
|
|
|
|
cfg.save();
|
|
|
|
}
|
|
|
|
|
2016-06-21 01:14:07 +02:00
|
|
|
void CreateWithFilesystemID(const CryConfig::FilesystemID &filesystemId, const string &password = "mypassword") {
|
2016-06-27 01:53:10 +02:00
|
|
|
auto cfg = loader(password, false).loadOrCreate(file.path()).value().configFile;
|
2016-06-21 01:14:07 +02:00
|
|
|
cfg.config()->SetFilesystemId(filesystemId);
|
|
|
|
cfg.save();
|
|
|
|
}
|
|
|
|
|
2017-09-30 23:24:33 +02:00
|
|
|
void ChangeFilesystemID(const CryConfig::FilesystemID &filesystemId, const string& password = "mypassword") {
|
2017-09-30 23:44:24 +02:00
|
|
|
auto cfg = CryConfigFile::load(file.path(), password).value();
|
2017-09-30 23:24:33 +02:00
|
|
|
cfg.config()->SetFilesystemId(filesystemId);
|
|
|
|
cfg.save();
|
|
|
|
}
|
|
|
|
|
2016-05-04 05:34:30 +02:00
|
|
|
string olderVersion() {
|
|
|
|
string olderVersion;
|
|
|
|
if (std::stol(gitversion::MinorVersion()) > 0) {
|
|
|
|
olderVersion = gitversion::MajorVersion() + "." + std::to_string(std::stol(gitversion::MinorVersion()) - 1);
|
|
|
|
} else {
|
|
|
|
olderVersion = std::to_string(std::stol(gitversion::MajorVersion()) - 1) + "." + gitversion::MinorVersion();
|
|
|
|
}
|
|
|
|
assert(gitversion::VersionCompare::isOlderThan(olderVersion, gitversion::VersionString()));
|
|
|
|
return olderVersion;
|
|
|
|
}
|
|
|
|
|
|
|
|
string newerVersion() {
|
|
|
|
string newerVersion = gitversion::MajorVersion()+"."+std::to_string(std::stol(gitversion::MinorVersion())+1);
|
|
|
|
assert(gitversion::VersionCompare::isOlderThan(gitversion::VersionString(), newerVersion));
|
|
|
|
return newerVersion;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::shared_ptr<MockConsole> console;
|
2015-10-23 12:16:23 +02:00
|
|
|
TempFile file;
|
|
|
|
};
|
|
|
|
|
|
|
|
TEST_F(CryConfigLoaderTest, CreatesNewIfNotExisting) {
|
|
|
|
EXPECT_FALSE(file.exists());
|
|
|
|
Create();
|
|
|
|
EXPECT_TRUE(file.exists());
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(CryConfigLoaderTest, DoesntCrashIfExisting) {
|
|
|
|
Create();
|
|
|
|
Load();
|
|
|
|
}
|
|
|
|
|
2015-10-26 16:36:57 +01:00
|
|
|
TEST_F(CryConfigLoaderTest, DoesntLoadIfWrongPassword) {
|
2015-10-24 19:35:37 +02:00
|
|
|
Create("mypassword");
|
2015-10-26 16:36:57 +01:00
|
|
|
auto loaded = Load("mypassword2");
|
|
|
|
EXPECT_EQ(none, loaded);
|
2015-10-24 19:35:37 +02:00
|
|
|
}
|
|
|
|
|
2015-10-30 19:53:15 +01:00
|
|
|
TEST_F(CryConfigLoaderTest, DoesntLoadIfDifferentCipher) {
|
2016-02-21 01:34:21 +01:00
|
|
|
Create("mypassword", string("aes-256-gcm"), false);
|
2015-10-30 19:53:15 +01:00
|
|
|
try {
|
2016-02-21 01:34:21 +01:00
|
|
|
Load("mypassword", string("aes-256-cfb"), false);
|
|
|
|
EXPECT_TRUE(false); // Should throw exception
|
|
|
|
} catch (const std::runtime_error &e) {
|
|
|
|
EXPECT_EQ(string("Filesystem uses aes-256-gcm cipher and not aes-256-cfb as specified."), e.what());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(CryConfigLoaderTest, DoesntLoadIfDifferentCipher_Noninteractive) {
|
|
|
|
Create("mypassword", string("aes-256-gcm"), true);
|
|
|
|
try {
|
|
|
|
Load("mypassword", string("aes-256-cfb"), true);
|
2015-10-30 19:53:15 +01:00
|
|
|
EXPECT_TRUE(false); // Should throw exception
|
|
|
|
} catch (const std::runtime_error &e) {
|
|
|
|
EXPECT_EQ(string("Filesystem uses aes-256-gcm cipher and not aes-256-cfb as specified."), e.what());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(CryConfigLoaderTest, DoesLoadIfSameCipher) {
|
|
|
|
Create("mypassword", string("aes-256-gcm"));
|
|
|
|
Load("mypassword", string("aes-256-gcm"));
|
|
|
|
}
|
|
|
|
|
2016-02-21 01:34:21 +01:00
|
|
|
TEST_F(CryConfigLoaderTest, DoesLoadIfSameCipher_Noninteractive) {
|
|
|
|
Create("mypassword", string("aes-128-gcm"), true);
|
|
|
|
Load("mypassword", string("aes-128-gcm"), true);
|
|
|
|
}
|
|
|
|
|
2015-10-23 12:16:23 +02:00
|
|
|
TEST_F(CryConfigLoaderTest, RootBlob_Load) {
|
|
|
|
CreateWithRootBlob("rootblobid");
|
2015-10-26 16:36:57 +01:00
|
|
|
auto loaded = Load().value();
|
2015-10-23 12:16:23 +02:00
|
|
|
EXPECT_EQ("rootblobid", loaded.config()->RootBlob());
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(CryConfigLoaderTest, RootBlob_Create) {
|
|
|
|
auto created = Create();
|
|
|
|
EXPECT_EQ("", created.config()->RootBlob());
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(CryConfigLoaderTest, EncryptionKey_Load) {
|
2017-09-30 10:30:31 +02:00
|
|
|
CreateWithEncryptionKey("3B4682CF22F3CA199E385729B9F3CA19D325229E385729B9443CA19D325229E3");
|
2015-10-26 16:36:57 +01:00
|
|
|
auto loaded = Load().value();
|
2017-09-30 10:30:31 +02:00
|
|
|
EXPECT_EQ("3B4682CF22F3CA199E385729B9F3CA19D325229E385729B9443CA19D325229E3", loaded.config()->EncryptionKey());
|
2015-10-23 12:16:23 +02:00
|
|
|
}
|
|
|
|
|
2017-09-30 23:24:33 +02:00
|
|
|
TEST_F(CryConfigLoaderTest, EncryptionKey_Load_whenKeyChanged_thenFails) {
|
|
|
|
CreateWithEncryptionKey("3B4682CF22F3CA199E385729B9F3CA19D325229E385729B9443CA19D325229E3");
|
|
|
|
ChangeEncryptionKey("3B4682CF22F3CA199E385729B9F3CA19D325229E385729B9443CA19D325229E4");
|
|
|
|
EXPECT_THROW(
|
|
|
|
Load(),
|
|
|
|
std::runtime_error
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2015-10-23 12:16:23 +02:00
|
|
|
TEST_F(CryConfigLoaderTest, EncryptionKey_Create) {
|
|
|
|
auto created = Create();
|
|
|
|
//aes-256-gcm is the default cipher chosen by mockConsole()
|
2015-10-27 23:46:54 +01:00
|
|
|
cpputils::AES256_GCM::EncryptionKey::FromString(created.config()->EncryptionKey()); // This crashes if key is invalid
|
2015-10-23 12:16:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(CryConfigLoaderTest, Cipher_Load) {
|
2015-11-11 10:19:47 +01:00
|
|
|
CreateWithCipher("twofish-128-cfb");
|
2015-10-26 16:36:57 +01:00
|
|
|
auto loaded = Load().value();
|
2015-11-11 20:58:09 +01:00
|
|
|
EXPECT_EQ("twofish-128-cfb", loaded.config()->Cipher());
|
2015-10-23 12:16:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(CryConfigLoaderTest, Cipher_Create) {
|
|
|
|
auto created = Create();
|
|
|
|
//aes-256-gcm is the default cipher chosen by mockConsole()
|
|
|
|
EXPECT_EQ("aes-256-gcm", created.config()->Cipher());
|
|
|
|
}
|
2016-03-26 17:09:07 +01:00
|
|
|
|
|
|
|
TEST_F(CryConfigLoaderTest, Version_Load) {
|
|
|
|
CreateWithVersion("0.9.2");
|
|
|
|
auto loaded = Load().value();
|
|
|
|
EXPECT_EQ(gitversion::VersionString(), loaded.config()->Version());
|
|
|
|
EXPECT_EQ("0.9.2", loaded.config()->CreatedWithVersion());
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(CryConfigLoaderTest, Version_Load_IsStoredAndNotOnlyOverwrittenInMemoryOnLoad) {
|
|
|
|
CreateWithVersion("0.9.2", "mypassword");
|
|
|
|
Load().value();
|
|
|
|
auto configFile = CryConfigFile::load(file.path(), "mypassword").value();
|
|
|
|
EXPECT_EQ(gitversion::VersionString(), configFile.config()->Version());
|
|
|
|
EXPECT_EQ("0.9.2", configFile.config()->CreatedWithVersion());
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(CryConfigLoaderTest, Version_Create) {
|
|
|
|
auto created = Create();
|
|
|
|
EXPECT_EQ(gitversion::VersionString(), created.config()->Version());
|
|
|
|
EXPECT_EQ(gitversion::VersionString(), created.config()->CreatedWithVersion());
|
|
|
|
}
|
2016-05-04 05:34:30 +02:00
|
|
|
|
2016-06-21 01:14:07 +02:00
|
|
|
TEST_F(CryConfigLoaderTest, FilesystemID_Load) {
|
|
|
|
auto fixture = DataFixture::generateFixedSize<CryConfig::FilesystemID::BINARY_LENGTH>();
|
|
|
|
CreateWithFilesystemID(fixture);
|
|
|
|
auto loaded = Load().value();
|
|
|
|
EXPECT_EQ(fixture, loaded.config()->FilesystemId());
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(CryConfigLoaderTest, FilesystemID_Create) {
|
|
|
|
auto created = Create();
|
|
|
|
EXPECT_NE(CryConfig::FilesystemID::Null(), created.config()->FilesystemId());
|
|
|
|
}
|
|
|
|
|
2016-06-07 21:52:06 +02:00
|
|
|
TEST_F(CryConfigLoaderTest, AsksWhenLoadingNewerFilesystem_AnswerYes) {
|
2016-09-25 02:50:28 +02:00
|
|
|
EXPECT_CALL(*console, askYesNo(HasSubstr("should not be opened with older versions"), false)).Times(1).WillOnce(Return(true));
|
2016-06-07 21:52:06 +02:00
|
|
|
|
|
|
|
string version = newerVersion();
|
|
|
|
CreateWithVersion(version);
|
|
|
|
EXPECT_NE(boost::none, Load());
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(CryConfigLoaderTest, AsksWhenLoadingNewerFilesystem_AnswerNo) {
|
2016-09-25 02:50:28 +02:00
|
|
|
EXPECT_CALL(*console, askYesNo(HasSubstr("should not be opened with older versions"), false)).Times(1).WillOnce(Return(false));
|
2016-06-07 21:52:06 +02:00
|
|
|
|
2016-05-04 05:34:30 +02:00
|
|
|
string version = newerVersion();
|
|
|
|
CreateWithVersion(version);
|
|
|
|
try {
|
|
|
|
Load();
|
|
|
|
EXPECT_TRUE(false); // expect throw
|
|
|
|
} catch (const std::runtime_error &e) {
|
2016-09-25 11:41:21 +02:00
|
|
|
EXPECT_THAT(e.what(), HasSubstr("Please update your CryFS version."));
|
2016-05-04 05:34:30 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(CryConfigLoaderTest, AsksWhenMigratingOlderFilesystem) {
|
2016-09-25 02:50:28 +02:00
|
|
|
EXPECT_CALL(*console, askYesNo(HasSubstr("Do you want to migrate it?"), false)).Times(1).WillOnce(Return(true));
|
2016-05-04 05:34:30 +02:00
|
|
|
|
|
|
|
string version = olderVersion();
|
|
|
|
CreateWithVersion(version);
|
|
|
|
EXPECT_NE(boost::none, Load());
|
|
|
|
}
|
|
|
|
|
2017-09-30 22:35:02 +02:00
|
|
|
TEST_F(CryConfigLoaderTest, DoesNotAskForMigrationWhenCorrectVersion) {
|
|
|
|
EXPECT_CALL(*console, askYesNo(HasSubstr("Do you want to migrate it?"), false)).Times(0);
|
|
|
|
|
|
|
|
CreateWithVersion(gitversion::VersionString());
|
|
|
|
EXPECT_NE(boost::none, Load());
|
|
|
|
}
|
2016-05-04 05:34:30 +02:00
|
|
|
|
2017-09-30 22:35:02 +02:00
|
|
|
TEST_F(CryConfigLoaderTest, DontMigrateWhenAnsweredNo) {
|
|
|
|
EXPECT_CALL(*console, askYesNo(HasSubstr("Do you want to migrate it?"), false)).Times(1).WillOnce(Return(false));
|
|
|
|
|
|
|
|
string version = olderVersion();
|
|
|
|
CreateWithVersion(version);
|
2016-05-04 05:34:30 +02:00
|
|
|
try {
|
|
|
|
Load();
|
|
|
|
EXPECT_TRUE(false); // expect throw
|
|
|
|
} catch (const std::runtime_error &e) {
|
2016-09-25 11:41:21 +02:00
|
|
|
EXPECT_THAT(e.what(), HasSubstr("It has to be migrated."));
|
2016-05-04 05:34:30 +02:00
|
|
|
}
|
|
|
|
}
|
2016-06-27 02:02:51 +02:00
|
|
|
|
|
|
|
TEST_F(CryConfigLoaderTest, MyClientIdIsIndeterministic) {
|
|
|
|
TempFile file1(false);
|
|
|
|
TempFile file2(false);
|
|
|
|
uint32_t myClientId = loader("mypassword", true).loadOrCreate(file1.path()).value().myClientId;
|
|
|
|
EXPECT_NE(myClientId, loader("mypassword", true).loadOrCreate(file2.path()).value().myClientId);
|
|
|
|
}
|
2017-09-30 19:53:03 +02:00
|
|
|
|
2016-06-27 02:02:51 +02:00
|
|
|
TEST_F(CryConfigLoaderTest, MyClientIdIsLoadedCorrectly) {
|
|
|
|
TempFile file(false);
|
|
|
|
uint32_t myClientId = loader("mypassword", true).loadOrCreate(file.path()).value().myClientId;
|
|
|
|
EXPECT_EQ(myClientId, loader("mypassword", true).loadOrCreate(file.path()).value().myClientId);
|
|
|
|
}
|