#include "app.h"
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <memory>
#include <string>
#include <string_view>
#include <Magick++.h>
#include <gtest/gtest.h>
#include <httplib.h>
#include "non_secret_random.h"
#include "startup.h"
namespace
{
std::string readFile(const std::filesystem::path& path)
{
std::ifstream input(path, std::ios::binary);
return std::string(
std::istreambuf_iterator<char>(input),
std::istreambuf_iterator<char>());
}
std::string hiddenValue(const std::string& body, std::string_view name)
{
std::string marker = "name=\"" + std::string(name) + "\" value=\"";
std::size_t begin = body.find(marker);
if(begin == std::string::npos)
{
marker = "name=" + std::string(name) + " value=\"";
begin = body.find(marker);
}
if(begin == std::string::npos)
{
return {};
}
const std::size_t value_begin = begin + marker.size();
const std::size_t end = body.find('"', value_begin);
return end == std::string::npos
? std::string()
: body.substr(value_begin, end - value_begin);
}
std::string responseCookie(
const httplib::Response& response, std::string_view name)
{
const std::string prefix = std::string(name) + '=';
for(std::size_t index = 0;
index < response.get_header_value_count("Set-Cookie");
++index)
{
const std::string value = response.get_header_value(
"Set-Cookie", nullptr, index);
if(value.starts_with(prefix))
{
return value.substr(0, value.find(';'));
}
}
return {};
}
std::string urlRequestPath(const std::string& url)
{
const std::size_t scheme = url.find("://");
const std::size_t path = scheme == std::string::npos
? std::string::npos
: url.find('/', scheme + 3);
if(path == std::string::npos)
{
return {};
}
const std::size_t end = url.find_first_of("\r\n\t ", path);
return url.substr(path, end - path);
}
class TemporaryServerRoot
{
public:
/// Allocate private database and card-storage paths.
TemporaryServerRoot()
: path_(
std::filesystem::path(testing::TempDir()) /
("card_server_" + std::to_string(
std::chrono::steady_clock::now()
.time_since_epoch().count())))
{
std::filesystem::create_directories(path_ / "cards/published");
}
/// Remove all temporary server state.
~TemporaryServerRoot()
{
std::error_code error;
std::filesystem::remove_all(path_, error);
}
/// Return the temporary root path.
const std::filesystem::path& path() const
{
return path_;
}
private:
std::filesystem::path path_;
};
} // namespace
/// Verify real HTTP routing, base paths, and independent static mounts.
TEST(AppIntegrationTest, ServesPagesAndStaticAssets)
{
Magick::InitializeMagick(nullptr);
Magick::ResourceLimits::listLength(2);
Magick::ResourceLimits::width(2048);
Magick::ResourceLimits::height(2048);
TemporaryServerRoot temporary;
const int port = 39000 + static_cast<int>(
std::chrono::steady_clock::now().time_since_epoch().count() % 1000);
auto base_url = mw::URL::fromStr(
"http://127.0.0.1:" + std::to_string(port) + "/collection/");
ASSERT_TRUE(base_url);
Config config = {
std::move(*base_url),
mw::IPSocketInfo{"127.0.0.1", port},
std::filesystem::path(CARD_COLLECTION_SOURCE_DIR) / "static",
temporary.path() / "cards.sqlite3",
temporary.path() / "cards",
75,
256,
};
config.email.link_file = temporary.path() / "latest-link.txt";
auto data_source = prepareDataSource(config.database_path);
ASSERT_TRUE(data_source);
auto transaction = (*data_source)->beginTransaction();
ASSERT_TRUE(transaction);
ASSERT_TRUE((*transaction)->insertGame(
{"test", "Test Game", "Dynamic extension test game.",
GameVisibility::PUBLIC, 1}));
auto hp_id = (*transaction)->insertGameField(
{0, "test", "hp", "HP", GameFieldType::INTEGER, 0, {}}, {});
auto attack_id = (*transaction)->insertGameField(
{0, "test", "attack", "Attack", GameFieldType::INTEGER, 1, {}},
{});
ASSERT_TRUE(hp_id);
ASSERT_TRUE(attack_id);
auto series_id = (*transaction)->insertSeries(
{0, "test", "Core <Set>", "A **series**."});
ASSERT_TRUE(series_id);
auto number = (*transaction)->allocateGameNumber("test");
ASSERT_TRUE(number);
auto card_id = (*transaction)->insertCard(
{
0,
{"test", *number},
"Card <One>",
std::nullopt,
std::nullopt,
0,
"avif",
std::nullopt,
"avif",
1,
1,
},
{{*hp_id, GameFieldType::INTEGER, std::int64_t{20}},
{*attack_id, GameFieldType::INTEGER, std::int64_t{5}}},
{*series_id});
ASSERT_TRUE(card_id);
ASSERT_TRUE((*transaction)->commit());
const std::filesystem::path card_directory =
config.card_storage_root / "published/test-1";
std::filesystem::create_directories(card_directory);
std::ofstream(card_directory / "front-art.avif") << "front";
std::ofstream(card_directory / "thumb.avif") << "thumbnail";
const std::filesystem::path upload_path =
temporary.path() / "upload.png";
Magick::Image upload(
Magick::Geometry(350, 490), Magick::Color("navy"));
upload.write("PNG:" + upload_path.string());
const std::string upload_bytes = readFile(upload_path);
App app(
config,
std::move(*data_source),
std::make_unique<NonSecretRandom>(1));
ASSERT_TRUE(app.start());
httplib::Client client("127.0.0.1", port);
auto index = client.Get("/collection/");
ASSERT_NE(index, nullptr);
EXPECT_EQ(index->status, 200);
EXPECT_NE(index->body.find("Card Collection"), std::string::npos);
auto anonymous_card = client.Get("/collection/cards/test-1");
ASSERT_NE(anonymous_card, nullptr);
EXPECT_EQ(anonymous_card->status, 303);
auto authentication = client.Get("/collection/authentication");
ASSERT_NE(authentication, nullptr);
ASSERT_EQ(authentication->status, 200);
const std::string authentication_cookie = responseCookie(
*authentication, "card_collection_auth_form");
const std::string authentication_nonce = hiddenValue(
authentication->body, "form_nonce");
ASSERT_FALSE(authentication_cookie.empty());
ASSERT_FALSE(authentication_nonce.empty());
const httplib::Headers authentication_headers = {
{"Cookie", authentication_cookie}};
const httplib::Params email_fields = {
{"email", "admin@example.com"},
{"form_nonce", authentication_nonce}};
auto email = client.Post(
"/collection/authentication/email",
authentication_headers,
email_fields);
ASSERT_NE(email, nullptr);
ASSERT_EQ(email->status, 303) << email->body;
const std::string confirmation_path = urlRequestPath(
readFile(config.email.link_file));
ASSERT_FALSE(confirmation_path.empty());
auto limited_form = client.Get("/collection/authentication");
ASSERT_NE(limited_form, nullptr);
const httplib::Headers limited_headers = {{
"Cookie",
responseCookie(*limited_form, "card_collection_auth_form")}};
const httplib::Params limited_fields = {
{"email", "ADMIN@example.com"},
{"form_nonce", hiddenValue(limited_form->body, "form_nonce")}};
auto limited = client.Post(
"/collection/authentication/email",
limited_headers,
limited_fields);
ASSERT_NE(limited, nullptr);
EXPECT_EQ(limited->status, 429);
EXPECT_FALSE(limited->get_header_value("Retry-After").empty());
auto confirmation = client.Get(confirmation_path);
ASSERT_NE(confirmation, nullptr);
ASSERT_EQ(confirmation->status, 200) << confirmation->body;
EXPECT_EQ(
confirmation->get_header_value("Referrer-Policy"), "no-referrer");
const std::string confirmation_cookie = responseCookie(
*confirmation, "card_collection_confirm_form");
const std::string confirmation_nonce = hiddenValue(
confirmation->body, "form_nonce");
const httplib::Headers confirmation_headers = {
{"Cookie", confirmation_cookie}};
const httplib::Params confirmation_fields = {
{"form_nonce", confirmation_nonce}};
auto confirmed = client.Post(
confirmation_path,
confirmation_headers,
confirmation_fields);
ASSERT_NE(confirmed, nullptr);
ASSERT_EQ(confirmed->status, 303) << confirmed->body;
const std::string session_cookie = responseCookie(
*confirmed, "card_collection_session");
ASSERT_FALSE(session_cookie.empty());
EXPECT_NE(
confirmed->get_header_value("Set-Cookie").find("HttpOnly"),
std::string::npos);
const httplib::Headers session_headers = {{"Cookie", session_cookie}};
auto onboarding = client.Get(
"/collection/onboarding/username", session_headers);
ASSERT_NE(onboarding, nullptr);
ASSERT_EQ(onboarding->status, 200) << onboarding->body;
const std::string csrf_token = hiddenValue(
onboarding->body, "csrf_token");
ASSERT_FALSE(csrf_token.empty());
const httplib::Params onboarding_fields = {
{"username", "Administrator"}, {"csrf_token", csrf_token}};
auto onboarded = client.Post(
"/collection/onboarding/username",
session_headers,
onboarding_fields);
ASSERT_NE(onboarded, nullptr);
ASSERT_EQ(onboarded->status, 303) << onboarded->body;
auto administrator_collection = client.Get(
"/collection/collection", session_headers);
ASSERT_NE(administrator_collection, nullptr);
ASSERT_EQ(administrator_collection->status, 200)
<< administrator_collection->body;
EXPECT_NE(
administrator_collection->body.find("/collection/admin/cards"),
std::string::npos);
EXPECT_NE(
administrator_collection->body.find("/collection/admin/series"),
std::string::npos);
EXPECT_NE(
administrator_collection->body.find("/collection/admin/games"),
std::string::npos);
EXPECT_NE(
administrator_collection->body.find("/collection/admin/users"),
std::string::npos);
auto card = client.Get("/collection/cards/test-1", session_headers);
ASSERT_NE(card, nullptr);
EXPECT_EQ(card->status, 200) << card->body;
EXPECT_NE(card->body.find("Card <One>"), std::string::npos);
EXPECT_NE(card->body.find("Not currently"), std::string::npos);
auto create = client.Get("/collection/cards/new", session_headers);
ASSERT_NE(create, nullptr);
EXPECT_EQ(create->status, 200) << create->body;
EXPECT_NE(create->body.find("Test Game"), std::string::npos);
EXPECT_EQ(create->body.find("Going Home"), std::string::npos);
auto edit = client.Get(
"/collection/cards/test-1/edit", session_headers);
ASSERT_NE(edit, nullptr);
EXPECT_EQ(edit->status, 200) << edit->body;
EXPECT_NE(edit->body.find("value=\"20\""), std::string::npos);
auto series = client.Get(
"/collection/admin/series", session_headers);
ASSERT_NE(series, nullptr);
EXPECT_EQ(series->status, 200) << series->body;
EXPECT_NE(series->body.find("Core <Set>"), std::string::npos);
auto games_page = client.Get(
"/collection/admin/games", session_headers);
ASSERT_NE(games_page, nullptr);
EXPECT_EQ(games_page->status, 200) << games_page->body;
EXPECT_NE(games_page->body.find("Test Game"), std::string::npos);
EXPECT_NE(games_page->body.find("Dynamic extension test game."),
std::string::npos);
EXPECT_NE(games_page->body.find("HP"), std::string::npos);
EXPECT_EQ(games_page->body.find("Going Home"), std::string::npos);
auto stylesheet = client.Get("/collection/static/css/styles.css");
ASSERT_NE(stylesheet, nullptr);
EXPECT_EQ(stylesheet->status, 200);
auto card_asset = client.Get(
"/collection/static-cards/test-1/front-art.avif");
ASSERT_NE(card_asset, nullptr);
EXPECT_EQ(card_asset->status, 200);
EXPECT_EQ(card_asset->body, "front");
auto traversal = client.Get(
"/collection/static-cards/../cards.sqlite3");
ASSERT_NE(traversal, nullptr);
EXPECT_EQ(traversal->status, 404);
const httplib::Params csrf_fields = {{"csrf_token", csrf_token}};
const httplib::Params oversized_game_fields = {
{"csrf_token", csrf_token},
{"short_name", "oversized"},
{"display_name", "Oversized"},
{"description", std::string(1024 * 1024 + 1, 'x')},
};
auto oversized_game = client.Post(
"/collection/admin/games",
session_headers,
oversized_game_fields);
ASSERT_NE(oversized_game, nullptr);
EXPECT_EQ(oversized_game->status, 413);
const httplib::Params dynamic_game_fields = {
{"csrf_token", csrf_token},
{"short_name", "dynamic"},
{"display_name", "Dynamic Game"},
{"description", "A **dynamic** game."},
{"visibility", "PUBLIC"},
};
const httplib::Params missing_csrf_game_fields = {
{"short_name", "forbidden"},
{"display_name", "Forbidden"},
{"description", ""},
};
auto missing_csrf_game = client.Post(
"/collection/admin/games",
session_headers,
missing_csrf_game_fields);
ASSERT_NE(missing_csrf_game, nullptr);
EXPECT_EQ(missing_csrf_game->status, 403);
const httplib::Params invalid_game_fields = {
{"csrf_token", csrf_token},
{"short_name", "Invalid-Game"},
{"display_name", "Invalid Game"},
{"description", ""},
{"visibility", "PUBLIC"},
};
auto invalid_game = client.Post(
"/collection/admin/games",
session_headers,
invalid_game_fields);
ASSERT_NE(invalid_game, nullptr);
EXPECT_EQ(invalid_game->status, 422) << invalid_game->body;
EXPECT_NE(invalid_game->body.find("Check the game details"),
std::string::npos);
EXPECT_NE(invalid_game->body.find(
"aria-describedby=\"GameShortNameError\""),
std::string::npos);
EXPECT_NE(invalid_game->body.find("Invalid-Game"),
std::string::npos);
const httplib::Params invalid_visibility_fields = {
{"csrf_token", csrf_token},
{"short_name", "private"},
{"display_name", "Private"},
{"description", ""},
{"visibility", "PRIVATE"},
};
auto invalid_visibility = client.Post(
"/collection/admin/games",
session_headers,
invalid_visibility_fields);
ASSERT_NE(invalid_visibility, nullptr);
EXPECT_EQ(invalid_visibility->status, 400);
httplib::Params duplicate_visibility_fields = invalid_visibility_fields;
duplicate_visibility_fields.erase("visibility");
duplicate_visibility_fields.emplace("visibility", "PUBLIC");
duplicate_visibility_fields.emplace("visibility", "INTERNAL");
auto duplicate_visibility = client.Post(
"/collection/admin/games",
session_headers,
duplicate_visibility_fields);
ASSERT_NE(duplicate_visibility, nullptr);
EXPECT_EQ(duplicate_visibility->status, 400);
auto dynamic_game = client.Post(
"/collection/admin/games",
session_headers,
dynamic_game_fields);
ASSERT_NE(dynamic_game, nullptr);
ASSERT_EQ(dynamic_game->status, 303) << dynamic_game->body;
const httplib::Params invalid_field_fields = {
{"csrf_token", csrf_token},
{"game_revision", "1"},
{"key", "Invalid-Key"},
{"label", "Score"},
{"type", "INTEGER"},
};
auto invalid_field = client.Post(
"/collection/admin/games/dynamic/fields",
session_headers,
invalid_field_fields);
ASSERT_NE(invalid_field, nullptr);
EXPECT_EQ(invalid_field->status, 422) << invalid_field->body;
EXPECT_NE(invalid_field->body.find("Check the field definition"),
std::string::npos);
EXPECT_NE(invalid_field->body.find(
"aria-describedby=\"FieldKeyError\""), std::string::npos);
EXPECT_NE(invalid_field->body.find("Invalid-Key"),
std::string::npos);
const httplib::Params integer_field_fields = {
{"csrf_token", csrf_token},
{"game_revision", "1"},
{"key", "score"},
{"label", "Score"},
{"type", "INTEGER"},
};
auto integer_field = client.Post(
"/collection/admin/games/dynamic/fields",
session_headers,
integer_field_fields);
ASSERT_NE(integer_field, nullptr);
ASSERT_EQ(integer_field->status, 303) << integer_field->body;
const httplib::Params string_field_fields = {
{"csrf_token", csrf_token},
{"game_revision", "2"},
{"key", "note"},
{"label", "Note"},
{"type", "STRING"},
};
auto string_field = client.Post(
"/collection/admin/games/dynamic/fields",
session_headers,
string_field_fields);
ASSERT_NE(string_field, nullptr);
ASSERT_EQ(string_field->status, 303) << string_field->body;
const httplib::Params choice_field_fields = {
{"csrf_token", csrf_token},
{"game_revision", "3"},
{"key", "route"},
{"label", "Route"},
{"type", "CHOICE"},
{"choice", "Forest & River"},
{"choice", "Coast+Town"},
};
auto choice_field = client.Post(
"/collection/admin/games/dynamic/fields",
session_headers,
choice_field_fields);
ASSERT_NE(choice_field, nullptr);
ASSERT_EQ(choice_field->status, 303) << choice_field->body;
const httplib::Params field_order_fields = {
{"csrf_token", csrf_token},
{"game_revision", "4"},
{"field_id", "5"},
{"field_id", "4"},
{"field_id", "3"},
};
auto fields_reordered = client.Post(
"/collection/admin/games/dynamic/field-order",
session_headers,
field_order_fields);
ASSERT_NE(fields_reordered, nullptr);
ASSERT_EQ(fields_reordered->status, 303) << fields_reordered->body;
const httplib::Params game_update_fields = {
{"csrf_token", csrf_token},
{"game_revision", "5"},
{"display_name", "Dynamic Renamed"},
{"description", "A **rendered** description."},
{"visibility", "PUBLIC"},
};
auto game_updated = client.Post(
"/collection/admin/games/dynamic",
session_headers,
game_update_fields);
ASSERT_NE(game_updated, nullptr);
ASSERT_EQ(game_updated->status, 303) << game_updated->body;
auto dynamic_games_page = client.Get(
"/collection/admin/games", session_headers);
ASSERT_NE(dynamic_games_page, nullptr);
ASSERT_EQ(dynamic_games_page->status, 200);
EXPECT_NE(dynamic_games_page->body.find("Dynamic Renamed"),
std::string::npos);
EXPECT_NE(dynamic_games_page->body.find("<strong>rendered</strong>"),
std::string::npos);
auto foreign_field = client.Get(
"/collection/admin/games/dynamic/fields/1/edit",
session_headers);
ASSERT_NE(foreign_field, nullptr);
EXPECT_EQ(foreign_field->status, 404);
const httplib::Params stale_game_fields = {
{"csrf_token", csrf_token},
{"game_revision", "4"},
{"display_name", "Stale name"},
{"description", ""},
{"visibility", "PUBLIC"},
};
auto stale_game = client.Post(
"/collection/admin/games/dynamic",
session_headers,
stale_game_fields);
ASSERT_NE(stale_game, nullptr);
EXPECT_EQ(stale_game->status, 409) << stale_game->body;
EXPECT_NE(stale_game->body.find("Nothing was saved"),
std::string::npos);
EXPECT_NE(stale_game->body.find(
"/collection/admin/games/dynamic/edit"), std::string::npos);
const httplib::Params dynamic_series_fields = {
{"csrf_token", csrf_token},
{"game", "dynamic"},
{"name", "Dynamic series"},
{"description", "A dynamic series."},
};
auto dynamic_series = client.Post(
"/collection/admin/series",
session_headers,
dynamic_series_fields);
ASSERT_NE(dynamic_series, nullptr);
ASSERT_EQ(dynamic_series->status, 303) << dynamic_series->body;
const httplib::UploadFormDataItems dynamic_card_fields = {
{"csrf_token", csrf_token, "", ""},
{"game", "dynamic", "", ""},
{"game_revision", "6", "", ""},
{"name", "Dynamic card", "", ""},
{"rarity", "7", "", ""},
{"game.score", "9007199254740993", "", ""},
{"game.note", "Line <one>\nLine two", "", ""},
{"game.route", "Forest & River", "", ""},
{"series_id", "2", "", ""},
{"source_mode", "files", "", ""},
{"front", upload_bytes, "card.png", "image/png"},
};
httplib::UploadFormDataItems invalid_dynamic_card_fields =
dynamic_card_fields;
for(httplib::UploadFormData& field : invalid_dynamic_card_fields)
{
if(field.name == "game.route")
{
field.content = "Unknown route";
}
}
auto invalid_dynamic_card = client.Post(
"/collection/cards",
session_headers,
invalid_dynamic_card_fields);
ASSERT_NE(invalid_dynamic_card, nullptr);
EXPECT_EQ(invalid_dynamic_card->status, 422)
<< invalid_dynamic_card->body;
EXPECT_NE(invalid_dynamic_card->body.find("Check your card details"),
std::string::npos);
EXPECT_NE(invalid_dynamic_card->body.find(
"aria-describedby=\"GameField-dynamic-route-error\""),
std::string::npos);
EXPECT_NE(invalid_dynamic_card->body.find("9007199254740993"),
std::string::npos);
EXPECT_NE(invalid_dynamic_card->body.find("must be selected again"),
std::string::npos);
auto dynamic_card = client.Post(
"/collection/cards", session_headers, dynamic_card_fields);
ASSERT_NE(dynamic_card, nullptr);
ASSERT_EQ(dynamic_card->status, 303) << dynamic_card->body;
const std::string dynamic_card_path = urlRequestPath(
dynamic_card->get_header_value("Location"));
EXPECT_EQ(dynamic_card_path, "/collection/cards/dynamic-1");
auto dynamic_card_page = client.Get(
dynamic_card_path, session_headers);
ASSERT_NE(dynamic_card_page, nullptr);
ASSERT_EQ(dynamic_card_page->status, 200) << dynamic_card_page->body;
EXPECT_NE(dynamic_card_page->body.find("9007199254740993"),
std::string::npos);
EXPECT_NE(dynamic_card_page->body.find("Line <one>\nLine two"),
std::string::npos);
EXPECT_NE(dynamic_card_page->body.find("Forest & River"),
std::string::npos);
const httplib::UploadFormDataItems dynamic_edit_fields = {
{"csrf_token", csrf_token, "", ""},
{"name", "Dynamic card edited", "", ""},
{"rarity", "8", "", ""},
{"revision", "1", "", ""},
{"game_revision", "6", "", ""},
{"game.score", "-7", "", ""},
{"game.note", "Whitespace kept", "", ""},
{"game.route", "Coast+Town", "", ""},
{"series_id", "2", "", ""},
{"source_mode", "files", "", ""},
{"front_action", "keep", "", ""},
{"foil_action", "keep", "", ""},
};
auto dynamic_edited = client.Post(
dynamic_card_path + "/edit",
session_headers,
dynamic_edit_fields);
ASSERT_NE(dynamic_edited, nullptr);
ASSERT_EQ(dynamic_edited->status, 303) << dynamic_edited->body;
dynamic_card_page = client.Get(dynamic_card_path, session_headers);
ASSERT_NE(dynamic_card_page, nullptr);
ASSERT_EQ(dynamic_card_page->status, 200);
EXPECT_NE(dynamic_card_page->body.find("Dynamic card edited"),
std::string::npos);
EXPECT_NE(dynamic_card_page->body.find("Coast+Town"),
std::string::npos);
const httplib::Params second_game_update_fields = {
{"csrf_token", csrf_token},
{"game_revision", "6"},
{"display_name", "Dynamic Final"},
{"description", "Final description."},
{"visibility", "PUBLIC"},
};
auto second_game_update = client.Post(
"/collection/admin/games/dynamic",
session_headers,
second_game_update_fields);
ASSERT_NE(second_game_update, nullptr);
ASSERT_EQ(second_game_update->status, 303)
<< second_game_update->body;
httplib::UploadFormDataItems stale_dynamic_edit_fields =
dynamic_edit_fields;
for(httplib::UploadFormData& field : stale_dynamic_edit_fields)
{
if(field.name == "name")
{
field.content = "Stale card change";
}
}
auto stale_card = client.Post(
dynamic_card_path + "/edit",
session_headers,
stale_dynamic_edit_fields);
ASSERT_NE(stale_card, nullptr);
EXPECT_EQ(stale_card->status, 409) << stale_card->body;
EXPECT_NE(stale_card->body.find("must be selected again"),
std::string::npos);
dynamic_card_page = client.Get(dynamic_card_path, session_headers);
ASSERT_NE(dynamic_card_page, nullptr);
EXPECT_EQ(dynamic_card_page->status, 200);
EXPECT_EQ(dynamic_card_page->body.find("Stale card change"),
std::string::npos);
EXPECT_NE(dynamic_card_page->body.find("Dynamic card edited"),
std::string::npos);
const httplib::Params used_choice_fields = {
{"csrf_token", csrf_token},
{"game_revision", "7"},
{"label", "Route"},
{"choice", "Forest & River"},
};
auto used_choice = client.Post(
"/collection/admin/games/dynamic/fields/5",
session_headers,
used_choice_fields);
ASSERT_NE(used_choice, nullptr);
EXPECT_EQ(used_choice->status, 409) << used_choice->body;
EXPECT_NE(used_choice->body.find("1 card"), std::string::npos);
auto dynamic_deleted = client.Post(
dynamic_card_path + "/delete", session_headers, csrf_fields);
ASSERT_NE(dynamic_deleted, nullptr);
ASSERT_EQ(dynamic_deleted->status, 303) << dynamic_deleted->body;
auto dynamic_series_deleted = client.Post(
"/collection/admin/series/2/delete",
session_headers,
csrf_fields);
ASSERT_NE(dynamic_series_deleted, nullptr);
ASSERT_EQ(dynamic_series_deleted->status, 303)
<< dynamic_series_deleted->body;
auto choice_after_card = client.Post(
"/collection/admin/games/dynamic/fields/5",
session_headers,
used_choice_fields);
ASSERT_NE(choice_after_card, nullptr);
ASSERT_EQ(choice_after_card->status, 303) << choice_after_card->body;
const httplib::Params delete_choice_field = {
{"csrf_token", csrf_token}, {"game_revision", "8"}};
const httplib::Params delete_string_field = {
{"csrf_token", csrf_token}, {"game_revision", "9"}};
const httplib::Params delete_integer_field = {
{"csrf_token", csrf_token}, {"game_revision", "10"}};
auto choice_deleted = client.Post(
"/collection/admin/games/dynamic/fields/5/delete",
session_headers,
delete_choice_field);
ASSERT_NE(choice_deleted, nullptr);
ASSERT_EQ(choice_deleted->status, 303) << choice_deleted->body;
auto string_deleted = client.Post(
"/collection/admin/games/dynamic/fields/4/delete",
session_headers,
delete_string_field);
ASSERT_NE(string_deleted, nullptr);
ASSERT_EQ(string_deleted->status, 303) << string_deleted->body;
auto integer_deleted = client.Post(
"/collection/admin/games/dynamic/fields/3/delete",
session_headers,
delete_integer_field);
ASSERT_NE(integer_deleted, nullptr);
ASSERT_EQ(integer_deleted->status, 303) << integer_deleted->body;
const httplib::Params delete_issued_game = {
{"csrf_token", csrf_token}, {"game_revision", "11"}};
auto issued_game = client.Post(
"/collection/admin/games/dynamic/delete",
session_headers,
delete_issued_game);
ASSERT_NE(issued_game, nullptr);
EXPECT_EQ(issued_game->status, 409) << issued_game->body;
const httplib::Params empty_game_fields = {
{"csrf_token", csrf_token},
{"short_name", "empty"},
{"display_name", "Empty Game"},
{"description", ""},
{"visibility", "PUBLIC"},
};
auto empty_game = client.Post(
"/collection/admin/games", session_headers, empty_game_fields);
ASSERT_NE(empty_game, nullptr);
ASSERT_EQ(empty_game->status, 303) << empty_game->body;
const httplib::Params delete_empty_game = {
{"csrf_token", csrf_token}, {"game_revision", "1"}};
auto empty_deleted = client.Post(
"/collection/admin/games/empty/delete",
session_headers,
delete_empty_game);
ASSERT_NE(empty_deleted, nullptr);
EXPECT_EQ(empty_deleted->status, 303) << empty_deleted->body;
const httplib::UploadFormDataItems invalid_loose_fields = {
{"csrf_token", csrf_token, "", ""},
{"game", "", "", ""},
{"game.route", "Forest", "", ""},
{"name", "Tampered loose card", "", ""},
{"rarity", "1", "", ""},
{"source_mode", "files", "", ""},
{"front", upload_bytes, "card.png", "image/png"},
};
auto invalid_loose = client.Post(
"/collection/cards", session_headers, invalid_loose_fields);
ASSERT_NE(invalid_loose, nullptr);
EXPECT_EQ(invalid_loose->status, 422) << invalid_loose->body;
const httplib::Params going_home_fields = {
{"csrf_token", csrf_token},
{"short_name", "gh"},
{"display_name", "Going Home"},
{"description", ""},
{"visibility", "PUBLIC"},
};
auto going_home = client.Post(
"/collection/admin/games", session_headers, going_home_fields);
ASSERT_NE(going_home, nullptr);
ASSERT_EQ(going_home->status, 303) << going_home->body;
const httplib::UploadFormDataItems create_fields = {
{"csrf_token", csrf_token, "", ""},
{"game", "gh", "", ""},
{"game_revision", "1", "", ""},
{"name", "Uploaded card", "", ""},
{"rarity", "3", "", ""},
{"source_mode", "files", "", ""},
{"front", upload_bytes, "card.png", "image/png"},
};
auto created = client.Post(
"/collection/cards", session_headers, create_fields);
ASSERT_NE(created, nullptr);
ASSERT_EQ(created->status, 303) << created->body;
const std::string location = created->get_header_value("Location");
const std::size_t path_begin = location.find("/collection/cards/");
ASSERT_NE(path_begin, std::string::npos);
const std::string created_path = location.substr(path_begin);
const std::string public_id = created_path.substr(
std::string("/collection/cards/").size());
EXPECT_EQ(public_id, "gh-1");
EXPECT_TRUE(std::filesystem::is_directory(
config.card_storage_root / "published" / public_id));
const httplib::UploadFormDataItems edit_fields = {
{"csrf_token", csrf_token, "", ""},
{"name", "Edited upload", "", ""},
{"rarity", "4", "", ""},
{"revision", "1", "", ""},
{"game_revision", "1", "", ""},
{"source_mode", "files", "", ""},
{"front_action", "keep", "", ""},
{"foil_action", "keep", "", ""},
};
auto edited = client.Post(
created_path + "/edit", session_headers, edit_fields);
ASSERT_NE(edited, nullptr);
EXPECT_EQ(edited->status, 303) << edited->body;
auto edited_page = client.Get(created_path, session_headers);
ASSERT_NE(edited_page, nullptr);
EXPECT_EQ(edited_page->status, 200) << edited_page->body;
EXPECT_NE(edited_page->body.find("Going Home"), std::string::npos);
EXPECT_NE(edited_page->body.find("Edited upload"), std::string::npos);
httplib::Params create_series_fields = {
{"csrf_token", csrf_token},
{"game", "gh"},
{"name", "Uploaded series"},
{"description", "Description"},
};
auto created_series = client.Post(
"/collection/admin/series", session_headers, create_series_fields);
ASSERT_NE(created_series, nullptr);
EXPECT_EQ(created_series->status, 303) << created_series->body;
httplib::Params edit_series_fields = {
{"csrf_token", csrf_token},
{"name", "Edited series"},
{"description", "Changed"},
};
auto edited_series = client.Post(
"/collection/admin/series/2", session_headers, edit_series_fields);
ASSERT_NE(edited_series, nullptr);
EXPECT_EQ(edited_series->status, 303) << edited_series->body;
auto deleted_series = client.Post(
"/collection/admin/series/2/delete",
session_headers,
csrf_fields);
ASSERT_NE(deleted_series, nullptr);
EXPECT_EQ(deleted_series->status, 303) << deleted_series->body;
auto player_authentication = client.Get("/collection/authentication");
ASSERT_NE(player_authentication, nullptr);
const std::string player_auth_cookie = responseCookie(
*player_authentication, "card_collection_auth_form");
const std::string player_auth_nonce = hiddenValue(
player_authentication->body, "form_nonce");
const httplib::Headers player_auth_headers = {
{"Cookie", player_auth_cookie}};
const httplib::Params player_email_fields = {
{"email", "player@example.com"},
{"form_nonce", player_auth_nonce}};
auto player_email = client.Post(
"/collection/authentication/email",
player_auth_headers,
player_email_fields);
ASSERT_NE(player_email, nullptr);
ASSERT_EQ(player_email->status, 303) << player_email->body;
const std::string player_confirmation_path = urlRequestPath(
readFile(config.email.link_file));
auto player_confirmation = client.Get(player_confirmation_path);
ASSERT_NE(player_confirmation, nullptr);
const std::string player_confirmation_cookie = responseCookie(
*player_confirmation, "card_collection_confirm_form");
const httplib::Headers player_confirmation_headers = {
{"Cookie", player_confirmation_cookie}};
const httplib::Params player_confirmation_fields = {
{"form_nonce", hiddenValue(
player_confirmation->body, "form_nonce")}};
auto player_confirmed = client.Post(
player_confirmation_path,
player_confirmation_headers,
player_confirmation_fields);
ASSERT_NE(player_confirmed, nullptr);
ASSERT_EQ(player_confirmed->status, 303) << player_confirmed->body;
const std::string player_session_cookie = responseCookie(
*player_confirmed, "card_collection_session");
const httplib::Headers player_headers = {
{"Cookie", player_session_cookie}};
auto player_onboarding = client.Get(
"/collection/onboarding/username", player_headers);
ASSERT_NE(player_onboarding, nullptr);
const std::string player_csrf = hiddenValue(
player_onboarding->body, "csrf_token");
const httplib::Params player_onboarding_fields = {
{"username", "Player"}, {"csrf_token", player_csrf}};
auto player_onboarded = client.Post(
"/collection/onboarding/username",
player_headers,
player_onboarding_fields);
ASSERT_NE(player_onboarded, nullptr);
ASSERT_EQ(player_onboarded->status, 303) << player_onboarded->body;
auto player_collection = client.Get(
"/collection/collection", player_headers);
ASSERT_NE(player_collection, nullptr);
ASSERT_EQ(player_collection->status, 200) << player_collection->body;
EXPECT_EQ(
player_collection->body.find("/collection/admin/cards"),
std::string::npos);
EXPECT_EQ(
player_collection->body.find("/collection/admin/series"),
std::string::npos);
EXPECT_EQ(
player_collection->body.find("/collection/admin/games"),
std::string::npos);
EXPECT_EQ(
player_collection->body.find("/collection/admin/users"),
std::string::npos);
auto player_card_new = client.Get(
"/collection/cards/new", player_headers);
ASSERT_NE(player_card_new, nullptr);
EXPECT_EQ(player_card_new->status, 403);
auto player_admin = client.Get(
"/collection/admin/users", player_headers);
ASSERT_NE(player_admin, nullptr);
EXPECT_EQ(player_admin->status, 403);
auto player_games = client.Get(
"/collection/admin/games", player_headers);
ASSERT_NE(player_games, nullptr);
EXPECT_EQ(player_games->status, 403);
const httplib::Params player_game_fields = {
{"csrf_token", player_csrf},
{"short_name", "playergame"},
{"display_name", "Player Game"},
{"description", ""},
{"visibility", "PUBLIC"},
};
auto player_game_create = client.Post(
"/collection/admin/games",
player_headers,
player_game_fields);
ASSERT_NE(player_game_create, nullptr);
EXPECT_EQ(player_game_create->status, 403);
const httplib::Params internal_game_fields = {
{"csrf_token", csrf_token},
{"game_revision", "1"},
{"display_name", "Going Home"},
{"description", ""},
{"visibility", "INTERNAL"},
};
auto internal_game = client.Post(
"/collection/admin/games/gh",
session_headers,
internal_game_fields);
ASSERT_NE(internal_game, nullptr);
ASSERT_EQ(internal_game->status, 303) << internal_game->body;
auto concealed_card = client.Get(created_path, player_headers);
ASSERT_NE(concealed_card, nullptr);
EXPECT_EQ(concealed_card->status, 404);
EXPECT_EQ(
concealed_card->get_header_value("Cache-Control"),
"private, no-store");
auto administrator_cards = client.Get(
"/collection/admin/cards", session_headers);
ASSERT_NE(administrator_cards, nullptr);
ASSERT_EQ(administrator_cards->status, 200);
EXPECT_NE(administrator_cards->body.find("Internal"),
std::string::npos);
auto administrator_card = client.Get(created_path, session_headers);
ASSERT_NE(administrator_card, nullptr);
EXPECT_EQ(administrator_card->status, 200);
EXPECT_NE(administrator_card->body.find(
"Not currently in the pull pool"), std::string::npos);
auto promoted = client.Post(
"/collection/admin/users/2/promote",
session_headers,
csrf_fields);
ASSERT_NE(promoted, nullptr);
ASSERT_EQ(promoted->status, 303) << promoted->body;
auto creator_game_create = client.Post(
"/collection/admin/games",
player_headers,
player_game_fields);
ASSERT_NE(creator_game_create, nullptr);
EXPECT_EQ(creator_game_create->status, 403);
auto creator_new = client.Get("/collection/cards/new", player_headers);
ASSERT_NE(creator_new, nullptr);
ASSERT_EQ(creator_new->status, 200) << creator_new->body;
EXPECT_EQ(creator_new->body.find("name=\"rarity\""), std::string::npos);
EXPECT_EQ(creator_new->body.find("Going Home"), std::string::npos);
const httplib::UploadFormDataItems forged_internal_fields = {
{"csrf_token", player_csrf, "", ""},
{"game", "gh", "", ""},
{"game_revision", "2", "", ""},
{"name", "Hidden creator card", "", ""},
{"source_mode", "files", "", ""},
{"front", upload_bytes, "card.png", "image/png"},
};
auto forged_internal = client.Post(
"/collection/cards", player_headers, forged_internal_fields);
ASSERT_NE(forged_internal, nullptr);
EXPECT_EQ(forged_internal->status, 422) << forged_internal->body;
const httplib::UploadFormDataItems creator_card_fields = {
{"csrf_token", player_csrf, "", ""},
{"game", "", "", ""},
{"name", "Creator draft", "", ""},
{"source_mode", "files", "", ""},
{"front", upload_bytes, "card.png", "image/png"},
};
auto creator_card = client.Post(
"/collection/cards", player_headers, creator_card_fields);
ASSERT_NE(creator_card, nullptr);
ASSERT_EQ(creator_card->status, 303) << creator_card->body;
const std::string creator_card_path = urlRequestPath(
creator_card->get_header_value("Location"));
auto creator_cards = client.Get(
"/collection/creator/cards", player_headers);
ASSERT_NE(creator_cards, nullptr);
EXPECT_EQ(creator_cards->status, 200) << creator_cards->body;
EXPECT_NE(creator_cards->body.find("Creator draft"), std::string::npos);
const httplib::UploadFormDataItems pool_edit_fields = {
{"csrf_token", csrf_token, "", ""},
{"name", "Creator draft", "", ""},
{"rarity", "2", "", ""},
{"revision", "1", "", ""},
{"source_mode", "files", "", ""},
{"front_action", "keep", "", ""},
{"foil_action", "keep", "", ""},
};
auto pooled = client.Post(
creator_card_path + "/edit", session_headers, pool_edit_fields);
ASSERT_NE(pooled, nullptr);
ASSERT_EQ(pooled->status, 303) << pooled->body;
const httplib::Params player_pull_fields = {
{"csrf_token", player_csrf}};
auto player_pull = client.Post(
"/collection/collection/pull", player_headers, player_pull_fields);
ASSERT_NE(player_pull, nullptr);
ASSERT_EQ(player_pull->status, 303) << player_pull->body;
EXPECT_EQ(
urlRequestPath(player_pull->get_header_value("Location")),
creator_card_path);
auto owned_card = client.Get(creator_card_path, player_headers);
ASSERT_NE(owned_card, nullptr);
EXPECT_EQ(owned_card->status, 200) << owned_card->body;
EXPECT_NE(owned_card->body.find("100%"), std::string::npos);
auto deleted = client.Post(
created_path + "/delete",
session_headers,
csrf_fields);
ASSERT_NE(deleted, nullptr);
EXPECT_EQ(deleted->status, 303) << deleted->body;
EXPECT_FALSE(std::filesystem::exists(
config.card_storage_root / "published" / public_id));
const httplib::Params player_logout_fields = {
{"csrf_token", player_csrf}};
auto logged_out = client.Post(
"/collection/logout", player_headers, player_logout_fields);
ASSERT_NE(logged_out, nullptr);
EXPECT_EQ(logged_out->status, 303) << logged_out->body;
auto after_logout = client.Get("/collection/collection", player_headers);
ASSERT_NE(after_logout, nullptr);
EXPECT_EQ(after_logout->status, 303);
app.stop();
app.wait();
}