#include "GameDependencies.h" #include "Character.h" #include "Database.h" namespace { // One character's xml and save generation, like the charxml row class SaveDatabase : public TestSQLDatabase { public: std::string xml = ""; uint64_t generation = 7; uint32_t saves = 0; std::vector audits; std::optional ClaimCharacterXml(const LWOOBJID) override { return CharacterXml{ xml, ++generation }; } bool SaveCharacterXml(const LWOOBJID, const std::string_view lxfml, const uint64_t expected) override { if (expected != generation) return false; xml = lxfml; generation = expected + 1; saves++; return true; } uint64_t GetCharacterSaveGeneration(const LWOOBJID) override { return generation; } // A dashboard edit void UpdateCharacterXml(const LWOOBJID, const std::string_view lxfml) override { xml = lxfml; generation++; } void InsertAuditLog(uint32_t, const std::string_view, const std::string_view action, const std::string_view, uint32_t, LWOOBJID) override { audits.push_back(std::string(action)); } }; } class StaleSaveGuardTest : public GameDependenciesTest { protected: SaveDatabase* db{}; void SetUp() override { SetUpDependencies(); db = new SaveDatabase(); Database::_setDatabase(db); } void TearDown() override { TearDownDependencies(); } }; TEST_F(StaleSaveGuardTest, LoadingClaimsTheCharacter) { Character character(1, nullptr); character.UpdateFromDatabase(); EXPECT_EQ(character.GetSaveGeneration(), 8u); EXPECT_EQ(db->generation, 8u); EXPECT_EQ(character.GetXMLData(), db->xml); } TEST_F(StaleSaveGuardTest, SavesMoveTheGenerationAlong) { Character character(1, nullptr); character.UpdateFromDatabase(); character.WriteToDatabase(); character.WriteToDatabase(); EXPECT_EQ(db->saves, 2u); EXPECT_EQ(character.GetSaveGeneration(), db->generation); EXPECT_FALSE(character.IsSaveRefused()); EXPECT_TRUE(db->audits.empty()); } TEST_F(StaleSaveGuardTest, AnotherWorldLoadingMakesTheOldOneStale) { Character oldWorld(1, nullptr); oldWorld.UpdateFromDatabase(); oldWorld.WriteToDatabase(); Character newWorld(1, nullptr); newWorld.UpdateFromDatabase(); newWorld.WriteToDatabase(); const auto newXml = db->xml; // The old world still has the player (a disconnect it noticed late) and saves: refused, the new data stays oldWorld.WriteToDatabase(); EXPECT_TRUE(oldWorld.IsSaveRefused()); EXPECT_EQ(db->xml, newXml); EXPECT_EQ(db->saves, 2u); ASSERT_EQ(db->audits.size(), 1u); EXPECT_EQ(db->audits[0], "stale_save_refused"); // It doesn't try again (or audit again) oldWorld.WriteToDatabase(); EXPECT_EQ(db->audits.size(), 1u); // The world that has the character keeps saving newWorld.WriteToDatabase(); EXPECT_FALSE(newWorld.IsSaveRefused()); EXPECT_EQ(db->saves, 3u); } TEST_F(StaleSaveGuardTest, DashboardEditWins) { Character character(1, nullptr); character.UpdateFromDatabase(); Database::Get()->UpdateCharacterXml(1, ""); character.WriteToDatabase(); EXPECT_TRUE(character.IsSaveRefused()); EXPECT_EQ(db->xml, ""); // Loading again (the player logs back in) picks the edit up and saves normally character.UpdateFromDatabase(); EXPECT_FALSE(character.IsSaveRefused()); EXPECT_EQ(character.GetXMLData(), ""); character.WriteToDatabase(); EXPECT_FALSE(character.IsSaveRefused()); }