Cockatrice/tests/deck_content_hash_test.cpp
Lukas Brübach 34e22df5ba [DeckSync] Match server rows by identity and hash the whole deck
Sync keyed local files to server rows by path, but the server names a row
after the deck, not after the file, so any deck whose file name and deck name
differ uploaded again on every run. A second pass then brought the server copy
back as an extra local file.

- Match by the sidecar's row id, then guid, then content, and only then by the
  server path, so renaming either side no longer duplicates a deck.
- Key the server tree by row id: the server allows several rows with the same
  name in one folder, and keying by path silently dropped all but one.
- Stop discarding rows whose name contains '/' ("U/W Control"). They stay
  matchable, and only the download file name is sanitised.
- Build the download path from the folder plus a sanitised row name, including
  every folder segment, so a hostile server name cannot escape the deck folder.
- Download a colliding server row under a unique name and count it, instead of
  skipping it without a trace.
- Hash the whole deck instead of the card hash: SHA-1 over the native deck
  string covers name, printings, banner, tokens and sideboard plans. The client
  and the server hash the exact bytes they exchange, so both agree.

Adds deck_content_hash_test for the client/server hashing invariant and for the
name-only change the card hash cannot see.
2026-11-05 12:51:01 +01:00

51 lines
1.7 KiB
C++

#include "gtest/gtest.h"
#include <QString>
#include <libcockatrice/deck_list/deck_list.h>
namespace
{
QString deckWithName(const QString &name)
{
return QStringLiteral(R"(<?xml version="1.0"?><cockatrice_deck version="1">)"
R"(<deckname>%1</deckname><comments></comments>)"
R"(<zone name="main"><card number="4" name="Lightning Bolt"/></zone>)"
R"(<zone name="side"><card number="2" name="Pyroblast"/></zone>)"
R"(</cockatrice_deck>)")
.arg(name);
}
} // namespace
TEST(DeckContentHashTest, MatchesNativeSerialisation)
{
DeckList deck;
ASSERT_TRUE(deck.loadFromString_Native(deckWithName(QStringLiteral("Burn"))));
EXPECT_EQ(deck.getContentHash(), DeckList::contentHash(deck.writeToString_Native()));
}
TEST(DeckContentHashTest, IsStableAcrossLoads)
{
DeckList first;
DeckList second;
ASSERT_TRUE(first.loadFromString_Native(deckWithName(QStringLiteral("Burn"))));
ASSERT_TRUE(second.loadFromString_Native(deckWithName(QStringLiteral("Burn"))));
EXPECT_EQ(first.getContentHash(), second.getContentHash());
}
TEST(DeckContentHashTest, CoversDeckName)
{
DeckList burn;
DeckList renamed;
ASSERT_TRUE(burn.loadFromString_Native(deckWithName(QStringLiteral("Burn"))));
ASSERT_TRUE(renamed.loadFromString_Native(deckWithName(QStringLiteral("Burn v2"))));
// The card hash only looks at card names and counts, so it cannot tell the
// two decks apart; the content hash must.
EXPECT_EQ(burn.getDeckHash(), renamed.getDeckHash());
EXPECT_NE(burn.getContentHash(), renamed.getContentHash());
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}