#include <chrono>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <mw/crypto_mock.hpp>
#include "collection.h"
#include "data_sqlite.h"
#include "startup.h"
#include "user_service.h"
namespace
{
class TemporaryDatabase
{
public:
TemporaryDatabase()
: path_(
std::filesystem::path(testing::TempDir()) /
("collection_service_" + std::to_string(
std::chrono::steady_clock::now()
.time_since_epoch().count()) + ".sqlite3"))
{}
~TemporaryDatabase()
{
std::error_code error;
std::filesystem::remove(path_, error);
std::filesystem::remove(path_.string() + "-shm", error);
std::filesystem::remove(path_.string() + "-wal", error);
}
const std::filesystem::path& path() const
{
return path_;
}
private:
std::filesystem::path path_;
};
class ClockMock final : public ClockInterface
{
public:
std::chrono::system_clock::time_point now() const override
{
return current;
}
std::chrono::system_clock::time_point current{};
};
mw::E<std::int64_t> insertPlayer(
DataSourceInterface& data_source,
const std::string& email,
std::uint32_t pulls = 1)
{
auto transaction = data_source.beginTransaction();
if(!transaction)
{
return std::unexpected(std::move(transaction.error()));
}
User user = {
0,
email,
email,
std::nullopt,
UserRole::PLAYER,
pulls,
0,
0};
auto id = (*transaction)->insertUser(user);
if(!id)
{
return std::unexpected(std::move(id.error()));
}
auto username = (*transaction)->updateUsername(
*id, email.substr(0, email.find('@')), email);
if(!username)
{
return std::unexpected(std::move(username.error()));
}
auto commit = (*transaction)->commit();
if(!commit)
{
return std::unexpected(std::move(commit.error()));
}
return *id;
}
mw::E<std::int64_t> insertPoolCard(
DataSourceInterface& data_source,
std::int64_t creator_user_id,
std::uint32_t number,
std::int64_t rarity)
{
auto transaction = data_source.beginTransaction();
if(!transaction)
{
return std::unexpected(std::move(transaction.error()));
}
Card card = {
0,
{std::nullopt, number},
"Pool card",
std::nullopt,
std::nullopt,
rarity,
"jpg",
std::nullopt,
"jpg",
1,
creator_user_id};
auto id = (*transaction)->insertCard(card, {}, {});
if(!id)
{
return std::unexpected(std::move(id.error()));
}
auto commit = (*transaction)->commit();
if(!commit)
{
return std::unexpected(std::move(commit.error()));
}
return *id;
}
} // namespace
TEST(UserServiceTest, OnboardsAndPermanentlyPromotesPlayer)
{
TemporaryDatabase database;
auto data_source = prepareDataSource(database.path());
ASSERT_TRUE(data_source);
auto administrator = (*data_source)->getUserByEmailKey(
"admin@example.com");
ASSERT_TRUE(administrator);
ASSERT_TRUE(*administrator);
auto player_id = insertPlayer(**data_source, "player@example.com");
ASSERT_TRUE(player_id);
UserService users(**data_source);
auto renamed = users.setUsername(*player_id, "Stra\xC3\x9F" "e");
ASSERT_TRUE(renamed) << renamed.error().msg();
EXPECT_EQ(renamed->username, "Stra\xC3\x9F" "e");
auto promoted = users.promote((**administrator).id, *player_id);
ASSERT_TRUE(promoted) << promoted.error().msg();
EXPECT_EQ(promoted->role, UserRole::CREATOR);
auto repeated = users.promote((**administrator).id, *player_id);
ASSERT_TRUE(repeated);
EXPECT_EQ(repeated->role, UserRole::CREATOR);
EXPECT_FALSE(users.promote(*player_id, (**administrator).id));
}
TEST(CollectionServiceTest, AccruesCapsAndPullsAtomically)
{
TemporaryDatabase database;
auto data_source = prepareDataSource(database.path());
ASSERT_TRUE(data_source);
auto administrator = (*data_source)->getUserByEmailKey(
"admin@example.com");
ASSERT_TRUE(administrator);
ASSERT_TRUE(*administrator);
auto player_id = insertPlayer(**data_source, "collector@example.com");
ASSERT_TRUE(player_id);
auto card_id = insertPoolCard(
**data_source, (**administrator).id, 42, 1);
ASSERT_TRUE(card_id);
ClockMock clock;
mw::CryptoMock crypto;
EXPECT_CALL(crypto, randomBytes(8))
.WillOnce(testing::Return(std::vector<std::byte>(8)));
CollectionService collection(**data_source, clock, crypto, 3);
auto pulled = collection.pull(*player_id);
ASSERT_TRUE(pulled) << pulled.error().msg();
EXPECT_EQ(pulled->card.id, *card_id);
EXPECT_EQ(pulled->quantity, 1);
EXPECT_FALSE(collection.pull(*player_id));
clock.current = std::chrono::system_clock::time_point(
std::chrono::days(10));
auto refreshed = collection.refresh(*player_id);
ASSERT_TRUE(refreshed);
EXPECT_EQ(refreshed->stored_pulls, 3);
EXPECT_EQ(refreshed->pull_refresh_day, 10);
clock.current = std::chrono::system_clock::time_point(
std::chrono::days(9));
refreshed = collection.refresh(*player_id);
ASSERT_TRUE(refreshed);
EXPECT_EQ(refreshed->stored_pulls, 3);
EXPECT_EQ(refreshed->pull_refresh_day, 10);
}
TEST(CollectionServiceTest, EmptyPoolPreservesRefreshedPull)
{
TemporaryDatabase database;
auto data_source = prepareDataSource(database.path());
ASSERT_TRUE(data_source);
auto player_id = insertPlayer(**data_source, "empty@example.com");
ASSERT_TRUE(player_id);
ClockMock clock;
mw::CryptoMock crypto;
CollectionService collection(**data_source, clock, crypto, 3);
EXPECT_FALSE(collection.pull(*player_id));
auto user = (*data_source)->getUser(*player_id);
ASSERT_TRUE(user);
ASSERT_TRUE(*user);
EXPECT_EQ((**user).stored_pulls, 1);
}