From ce7b771a7c36390708227a644725f864e9e5d802 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 08:40:50 +0000 Subject: [PATCH] test: add property and character reputation unit tests Agent-Logs-Url: https://github.com/DarkflameUniverse/DarkflameServer/sessions/603b2808-f042-447c-ba49-e4a8d9f87856 Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com> --- .../GameDatabase/TestSQL/TestSQLDatabase.h | 34 +++- .../dComponentsTests/CMakeLists.txt | 1 + .../PropertyReputationTests.cpp | 186 ++++++++++++++++++ 3 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 tests/dGameTests/dComponentsTests/PropertyReputationTests.cpp diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index 6381dd89..2170830b 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -1,6 +1,7 @@ #ifndef TESTSQLDATABASE_H #define TESTSQLDATABASE_H +#include #include #include "GameDatabase.h" @@ -113,11 +114,36 @@ class TestSQLDatabase : public GameDatabase { m_CharacterReputation[charId] = reputation; }; std::vector GetPropertyReputationContributions( - const LWOOBJID propertyId, const std::string& date) override { return {}; }; + const LWOOBJID propertyId, const std::string& date) override { + const auto key = std::make_pair(propertyId, date); + if (m_PropertyContributions.contains(key)) { + return m_PropertyContributions.at(key); + } + return {}; + }; void UpdatePropertyReputationContribution( const LWOOBJID propertyId, const LWOOBJID playerId, - const std::string& date, const uint32_t reputationGained) override {}; - void UpdatePropertyReputation(const LWOOBJID propertyId, const uint32_t reputation) override {}; + const std::string& date, const uint32_t reputationGained) override { + const auto key = std::make_pair(propertyId, date); + auto& entries = m_PropertyContributions[key]; + for (auto& entry : entries) { + if (entry.playerId == playerId) { + entry.reputationGained = reputationGained; + return; + } + } + entries.push_back({ playerId, reputationGained }); + }; + void UpdatePropertyReputation(const LWOOBJID propertyId, const uint32_t reputation) override { + m_PropertyReputation[propertyId] = reputation; + }; + // Test helper: retrieve the property reputation stored via UpdatePropertyReputation. + uint32_t GetPropertyReputation(const LWOOBJID propertyId) const { + if (m_PropertyReputation.contains(propertyId)) { + return m_PropertyReputation.at(propertyId); + } + return 0; + } bool IsNameInUse(const std::string_view name) override { return false; }; std::optional GetModel(const LWOOBJID modelID) override { return {}; } @@ -125,6 +151,8 @@ class TestSQLDatabase : public GameDatabase { std::optional GetUgcModel(const LWOOBJID ugcId) override { return {}; } private: std::unordered_map m_CharacterReputation; + std::unordered_map m_PropertyReputation; + std::map, std::vector> m_PropertyContributions; }; #endif //!TESTSQLDATABASE_H diff --git a/tests/dGameTests/dComponentsTests/CMakeLists.txt b/tests/dGameTests/dComponentsTests/CMakeLists.txt index f73f1214..0cb86b76 100644 --- a/tests/dGameTests/dComponentsTests/CMakeLists.txt +++ b/tests/dGameTests/dComponentsTests/CMakeLists.txt @@ -1,6 +1,7 @@ set(DCOMPONENTS_TESTS "DestroyableComponentTests.cpp" "PetComponentTests.cpp" + "PropertyReputationTests.cpp" "SimplePhysicsComponentTests.cpp" "SavingTests.cpp" ) diff --git a/tests/dGameTests/dComponentsTests/PropertyReputationTests.cpp b/tests/dGameTests/dComponentsTests/PropertyReputationTests.cpp new file mode 100644 index 00000000..f1e28f94 --- /dev/null +++ b/tests/dGameTests/dComponentsTests/PropertyReputationTests.cpp @@ -0,0 +1,186 @@ +#include "GameDependencies.h" +#include + +#include "Character.h" +#include "CharacterComponent.h" +#include "Database.h" +#include "Entity.h" +#include "GameDatabase/TestSQL/TestSQLDatabase.h" + +// --------------------------------------------------------------------------- +// CharacterComponent reputation tests +// --------------------------------------------------------------------------- +class CharacterReputationTest : public GameDependenciesTest { +protected: + std::unique_ptr entity; + std::unique_ptr character; + CharacterComponent* characterComponent = nullptr; + + void SetUp() override { + SetUpDependencies(); + entity = std::make_unique(1, GameDependenciesTest::info); + character = std::make_unique(1, nullptr); + entity->SetCharacter(character.get()); + character->SetEntity(entity.get()); + characterComponent = entity->AddComponent(-1, character.get(), UNASSIGNED_SYSTEM_ADDRESS); + } + + void TearDown() override { + entity->SetCharacter(nullptr); + entity.reset(); + character.reset(); + TearDownDependencies(); + } +}; + +// SetReputation must persist using the character's raw DB id (Character::GetID), +// not the runtime object id (Character::GetObjectID with the CHARACTER bit set). +TEST_F(CharacterReputationTest, SetReputationUsesCharacterDBId) { + constexpr int64_t repValue = 12345; + characterComponent->SetReputation(repValue); + + // Reputation must be stored at the DB char id (GetID() == 1). + EXPECT_EQ(Database::Get()->GetCharacterReputation(character->GetID()), repValue); +} + +// SetReputation / GetReputation round-trip via in-memory TestSQLDatabase. +TEST_F(CharacterReputationTest, ReputationRoundTrip) { + characterComponent->SetReputation(500); + + EXPECT_EQ(characterComponent->GetReputation(), 500); + EXPECT_EQ(Database::Get()->GetCharacterReputation(character->GetID()), 500); +} + +// The CharacterComponent constructor must load reputation from the DB using +// Character::GetID() as the lookup key. +TEST_F(CharacterReputationTest, LoadReputationFromDBOnConstruction) { + Database::Get()->SetCharacterReputation(character->GetID(), 9876); + + // Re-construct the component; the constructor reads from DB. + auto* freshComp = entity->AddComponent(-1, character.get(), UNASSIGNED_SYSTEM_ADDRESS); + EXPECT_EQ(freshComp->GetReputation(), 9876); +} + +// In the test context, Character::GetObjectID() returns LWOOBJID_EMPTY (0) because +// UpdateInfoFromDatabase() is never called for test characters. This mirrors the +// production scenario where GetObjectID() carries the CHARACTER bit and therefore +// differs from GetID(). Verify the two keys are treated independently. +TEST_F(CharacterReputationTest, DBIdAndObjectIdAreDistinctKeys) { + // Precondition: the two IDs must differ so the test is meaningful. + ASSERT_NE(character->GetID(), character->GetObjectID()); + + characterComponent->SetReputation(42); + + // Reputation stored at GetID(). + EXPECT_EQ(Database::Get()->GetCharacterReputation(character->GetID()), 42); + // No reputation stored at GetObjectID() (wrong key). + EXPECT_EQ(Database::Get()->GetCharacterReputation(character->GetObjectID()), 0); +} + +// --------------------------------------------------------------------------- +// TestSQLDatabase property reputation + contribution storage tests +// --------------------------------------------------------------------------- +class PropertyReputationDBTest : public GameDependenciesTest { +protected: + TestSQLDatabase* testDB = nullptr; + + void SetUp() override { + SetUpDependencies(); + testDB = dynamic_cast(Database::Get()); + ASSERT_NE(testDB, nullptr); + } + + void TearDown() override { + TearDownDependencies(); + } +}; + +// UpdatePropertyReputation / GetPropertyReputation round-trip. +TEST_F(PropertyReputationDBTest, PropertyReputationRoundTrip) { + constexpr LWOOBJID propertyId = 42; + Database::Get()->UpdatePropertyReputation(propertyId, 100); + EXPECT_EQ(testDB->GetPropertyReputation(propertyId), 100u); +} + +// Overwriting property reputation replaces the previous value. +TEST_F(PropertyReputationDBTest, PropertyReputationOverwrite) { + constexpr LWOOBJID propertyId = 42; + Database::Get()->UpdatePropertyReputation(propertyId, 100); + Database::Get()->UpdatePropertyReputation(propertyId, 250); + EXPECT_EQ(testDB->GetPropertyReputation(propertyId), 250u); +} + +// UpdatePropertyReputationContribution stores using the given player ID (expected +// to be the character DB id, not the runtime object id with bits set). +TEST_F(PropertyReputationDBTest, ContributionStoredByCharDBId) { + constexpr LWOOBJID propertyId = 5; + constexpr LWOOBJID charDbId = 1; // raw DB id, no object bits + const std::string date = "2024-01-01"; + + Database::Get()->UpdatePropertyReputationContribution(propertyId, charDbId, date, 10); + + const auto contributions = Database::Get()->GetPropertyReputationContributions(propertyId, date); + ASSERT_EQ(contributions.size(), 1u); + EXPECT_EQ(contributions[0].playerId, charDbId); + EXPECT_EQ(contributions[0].reputationGained, 10u); +} + +// A second UpdatePropertyReputationContribution for the same player must upsert +// (update the existing entry) rather than append a duplicate. +TEST_F(PropertyReputationDBTest, ContributionUpsertUpdatesExistingEntry) { + constexpr LWOOBJID propertyId = 5; + constexpr LWOOBJID charDbId = 1; + const std::string date = "2024-01-01"; + + Database::Get()->UpdatePropertyReputationContribution(propertyId, charDbId, date, 10); + Database::Get()->UpdatePropertyReputationContribution(propertyId, charDbId, date, 30); + + const auto contributions = Database::Get()->GetPropertyReputationContributions(propertyId, date); + ASSERT_EQ(contributions.size(), 1u); + EXPECT_EQ(contributions[0].reputationGained, 30u); +} + +// Contributions from different dates must not bleed into each other. +TEST_F(PropertyReputationDBTest, ContributionsAreIsolatedByDate) { + constexpr LWOOBJID propertyId = 5; + constexpr LWOOBJID charDbId = 1; + + Database::Get()->UpdatePropertyReputationContribution(propertyId, charDbId, "2024-01-01", 10); + Database::Get()->UpdatePropertyReputationContribution(propertyId, charDbId, "2024-01-02", 20); + + const auto day1 = Database::Get()->GetPropertyReputationContributions(propertyId, "2024-01-01"); + const auto day2 = Database::Get()->GetPropertyReputationContributions(propertyId, "2024-01-02"); + + ASSERT_EQ(day1.size(), 1u); + EXPECT_EQ(day1[0].reputationGained, 10u); + + ASSERT_EQ(day2.size(), 1u); + EXPECT_EQ(day2[0].reputationGained, 20u); +} + +// Multiple distinct players can each have their own contribution entry per property/date. +TEST_F(PropertyReputationDBTest, MultiplePlayerContributionsForSameProperty) { + constexpr LWOOBJID propertyId = 5; + const std::string date = "2024-01-01"; + + Database::Get()->UpdatePropertyReputationContribution(propertyId, 1, date, 10); + Database::Get()->UpdatePropertyReputationContribution(propertyId, 2, date, 20); + + const auto contributions = Database::Get()->GetPropertyReputationContributions(propertyId, date); + ASSERT_EQ(contributions.size(), 2u); + + const auto FindPlayerRep = [&](const LWOOBJID playerId) { + for (const auto& c : contributions) { + if (c.playerId == playerId) return c.reputationGained; + } + return 0u; + }; + EXPECT_EQ(FindPlayerRep(1), 10u); + EXPECT_EQ(FindPlayerRep(2), 20u); +} + +// Querying contributions for a date with no entries returns an empty vector. +TEST_F(PropertyReputationDBTest, NoContributionsReturnsEmpty) { + const auto contributions = Database::Get()->GetPropertyReputationContributions(99, "2024-01-01"); + EXPECT_TRUE(contributions.empty()); +}