1#include <base/detect.h>
2#include <base/str.h>
3#include <base/time.h>
4
5#include <engine/server/databases/connection.h>
6#include <engine/server/databases/connection_pool.h>
7#include <engine/shared/config.h>
8
9#include <game/server/scoreworker.h>
10
11#include <gmock/gmock.h>
12#include <gtest/gtest.h>
13#include <sqlite3.h>
14
15#if defined(CONF_TEST_MYSQL)
16int DummyMysqlInit = (MysqlInit(), 1);
17#endif
18
19TEST(SQLite, Version)
20{
21 ASSERT_GE(sqlite3_libversion_number(), 3025000) << "SQLite >= 3.25.0 required for Window functions";
22}
23
24struct Score : public testing::TestWithParam<IDbConnection *>
25{
26 Score()
27 {
28 Connect();
29 LoadBestTime();
30 InsertMap(pName: "Kobra 3", pMapper: "Zerodin", pServer: "Novice", Points: 5, Stars: 5);
31 }
32
33 ~Score()
34 {
35 m_pConn->Disconnect();
36 }
37
38 void Connect()
39 {
40 ASSERT_TRUE(m_pConn->Connect(m_aError, sizeof(m_aError))) << m_aError;
41
42 // Delete all existing entries for persistent databases like MySQL
43 int NumInserted = 0;
44 ASSERT_TRUE(m_pConn->PrepareStatement("DELETE FROM record_race", m_aError, sizeof(m_aError))) << m_aError;
45 ASSERT_TRUE(m_pConn->ExecuteUpdate(&NumInserted, m_aError, sizeof(m_aError))) << m_aError;
46 ASSERT_TRUE(m_pConn->PrepareStatement("DELETE FROM record_teamrace", m_aError, sizeof(m_aError))) << m_aError;
47 ASSERT_TRUE(m_pConn->ExecuteUpdate(&NumInserted, m_aError, sizeof(m_aError))) << m_aError;
48 ASSERT_TRUE(m_pConn->PrepareStatement("DELETE FROM record_maps", m_aError, sizeof(m_aError))) << m_aError;
49 ASSERT_TRUE(m_pConn->ExecuteUpdate(&NumInserted, m_aError, sizeof(m_aError))) << m_aError;
50 ASSERT_TRUE(m_pConn->PrepareStatement("DELETE FROM record_points", m_aError, sizeof(m_aError))) << m_aError;
51 ASSERT_TRUE(m_pConn->ExecuteUpdate(&NumInserted, m_aError, sizeof(m_aError))) << m_aError;
52 ASSERT_TRUE(m_pConn->PrepareStatement("DELETE FROM record_saves", m_aError, sizeof(m_aError))) << m_aError;
53 ASSERT_TRUE(m_pConn->ExecuteUpdate(&NumInserted, m_aError, sizeof(m_aError))) << m_aError;
54 }
55
56 void LoadBestTime()
57 {
58 CSqlLoadBestTimeRequest loadBestTimeReq(std::make_shared<CScoreLoadBestTimeResult>());
59 str_copy(dst: loadBestTimeReq.m_aMap, src: "Kobra 3", dst_size: sizeof(loadBestTimeReq.m_aMap));
60 ASSERT_TRUE(CScoreWorker::LoadBestTime(m_pConn, &loadBestTimeReq, m_aError, sizeof(m_aError))) << m_aError;
61 }
62
63 void InsertMap(const char *pName, const char *pMapper, const char *pServer, int Points, int Stars)
64 {
65 char aTimestamp[32];
66 str_timestamp_format(buffer: aTimestamp, buffer_size: sizeof(aTimestamp), format: TimestampFormat::SPACE);
67 char aBuf[512];
68 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
69 format: "%s into %s_maps(Map, Server, Mapper, Points, Stars, Timestamp) "
70 "VALUES (\"%s\", \"%s\", \"%s\", %d, %d, %s)",
71 m_pConn->InsertIgnore(), m_pConn->GetPrefix(), pName, pServer, pMapper, Points, Stars, m_pConn->InsertTimestampAsUtc());
72 ASSERT_TRUE(m_pConn->PrepareStatement(aBuf, m_aError, sizeof(m_aError))) << m_aError;
73 m_pConn->BindString(Idx: 1, pString: aTimestamp);
74 int NumInserted = 0;
75 ASSERT_TRUE(m_pConn->ExecuteUpdate(&NumInserted, m_aError, sizeof(m_aError))) << m_aError;
76 ASSERT_EQ(NumInserted, 1);
77 }
78
79 void InsertRank(float Time = 100.0, bool WithTimeCheckPoints = false)
80 {
81 str_copy(dst: g_Config.m_SvSqlServerName, src: "USA", dst_size: sizeof(g_Config.m_SvSqlServerName));
82 CSqlScoreData ScoreData(std::make_shared<CScorePlayerResult>());
83 str_copy(dst: ScoreData.m_aMap, src: "Kobra 3", dst_size: sizeof(ScoreData.m_aMap));
84 str_copy(dst: ScoreData.m_aGameUuid, src: "8d300ecf-5873-4297-bee5-95668fdff320", dst_size: sizeof(ScoreData.m_aGameUuid));
85 str_copy(dst: ScoreData.m_aName, src: "nameless tee", dst_size: sizeof(ScoreData.m_aName));
86 ScoreData.m_ClientId = 0;
87 ScoreData.m_Time = Time;
88 str_copy(dst: ScoreData.m_aTimestamp, src: "2021-11-24 19:24:08", dst_size: sizeof(ScoreData.m_aTimestamp));
89 for(int i = 0; i < NUM_CHECKPOINTS; i++)
90 ScoreData.m_aCurrentTimeCp[i] = WithTimeCheckPoints ? i : 0;
91 str_copy(dst: ScoreData.m_aRequestingPlayer, src: "deen", dst_size: sizeof(ScoreData.m_aRequestingPlayer));
92 ASSERT_TRUE(CScoreWorker::SaveScore(m_pConn, &ScoreData, Write::NORMAL, m_aError, sizeof(m_aError))) << m_aError;
93 }
94
95 void ExpectLines(const std::shared_ptr<CScorePlayerResult> &pPlayerResult, std::initializer_list<const char *> Lines, bool All = false)
96 {
97 EXPECT_EQ(pPlayerResult->m_MessageKind, All ? CScorePlayerResult::ALL : CScorePlayerResult::DIRECT);
98
99 int i = 0;
100 for(const char *pLine : Lines)
101 {
102 EXPECT_STREQ(pPlayerResult->m_Data.m_aaMessages[i], pLine);
103 i++;
104 }
105
106 for(; i < CScorePlayerResult::MAX_MESSAGES; i++)
107 {
108 EXPECT_STREQ(pPlayerResult->m_Data.m_aaMessages[i], "");
109 }
110 }
111
112 IDbConnection *m_pConn{GetParam()};
113 char m_aError[256] = {};
114 std::shared_ptr<CScorePlayerResult> m_pPlayerResult{std::make_shared<CScorePlayerResult>()};
115 CSqlPlayerRequest m_PlayerRequest{m_pPlayerResult};
116};
117
118struct SingleScore : public Score
119{
120 SingleScore()
121 {
122 InsertRank();
123 str_copy(dst: m_PlayerRequest.m_aMap, src: "Kobra 3", dst_size: sizeof(m_PlayerRequest.m_aMap));
124 str_copy(dst: m_PlayerRequest.m_aRequestingPlayer, src: "brainless tee", dst_size: sizeof(m_PlayerRequest.m_aRequestingPlayer));
125 m_PlayerRequest.m_Offset = 0;
126 str_copy(dst: m_PlayerRequest.m_aServer, src: "GER", dst_size: sizeof(m_PlayerRequest.m_aServer));
127 str_copy(dst: m_PlayerRequest.m_aName, src: "nameless tee", dst_size: sizeof(m_PlayerRequest.m_aMap));
128 }
129};
130
131TEST_P(SingleScore, TopRegional)
132{
133 g_Config.m_SvRegionalRankings = true;
134 ASSERT_TRUE(CScoreWorker::ShowTop(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
135 ExpectLines(pPlayerResult: m_pPlayerResult,
136 Lines: {"------------ Global Top ------------",
137 "1. nameless tee Time: 01:40.00",
138 "------------ GER Top ------------"});
139}
140
141TEST_P(SingleScore, Top)
142{
143 g_Config.m_SvRegionalRankings = false;
144 ASSERT_TRUE(CScoreWorker::ShowTop(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
145 ExpectLines(pPlayerResult: m_pPlayerResult,
146 Lines: {"------------ Global Top ------------",
147 "1. nameless tee Time: 01:40.00",
148 "-----------------------------------------"});
149}
150
151TEST_P(SingleScore, RankRegional)
152{
153 g_Config.m_SvRegionalRankings = true;
154 ASSERT_TRUE(CScoreWorker::ShowRank(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
155 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"nameless tee - 01:40.00 - better than 100% - requested by brainless tee", "Global rank 1 - GER unranked"}, All: true);
156}
157
158TEST_P(SingleScore, Rank)
159{
160 g_Config.m_SvRegionalRankings = false;
161 ASSERT_TRUE(CScoreWorker::ShowRank(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
162 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"nameless tee - 01:40.00 - better than 100% - requested by brainless tee", "Global rank 1"}, All: true);
163}
164
165TEST_P(SingleScore, TopServerRegional)
166{
167 g_Config.m_SvRegionalRankings = true;
168 str_copy(dst: m_PlayerRequest.m_aServer, src: "USA", dst_size: sizeof(m_PlayerRequest.m_aServer));
169 ASSERT_TRUE(CScoreWorker::ShowTop(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
170 ExpectLines(pPlayerResult: m_pPlayerResult,
171 Lines: {"------------ Global Top ------------",
172 "1. nameless tee Time: 01:40.00",
173 "------------ USA Top ------------",
174 "1. nameless tee Time: 01:40.00"});
175}
176
177TEST_P(SingleScore, TopServer)
178{
179 g_Config.m_SvRegionalRankings = false;
180 str_copy(dst: m_PlayerRequest.m_aServer, src: "USA", dst_size: sizeof(m_PlayerRequest.m_aServer));
181 ASSERT_TRUE(CScoreWorker::ShowTop(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
182 ExpectLines(pPlayerResult: m_pPlayerResult,
183 Lines: {"------------ Global Top ------------",
184 "1. nameless tee Time: 01:40.00",
185 "-----------------------------------------"});
186}
187
188TEST_P(SingleScore, RankServerRegional)
189{
190 g_Config.m_SvRegionalRankings = true;
191 str_copy(dst: m_PlayerRequest.m_aServer, src: "USA", dst_size: sizeof(m_PlayerRequest.m_aServer));
192 ASSERT_TRUE(CScoreWorker::ShowRank(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
193 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"nameless tee - 01:40.00 - better than 100% - requested by brainless tee", "Global rank 1 - USA rank 1"}, All: true);
194}
195
196TEST_P(SingleScore, RankServer)
197{
198 g_Config.m_SvRegionalRankings = false;
199 str_copy(dst: m_PlayerRequest.m_aServer, src: "USA", dst_size: sizeof(m_PlayerRequest.m_aServer));
200 ASSERT_TRUE(CScoreWorker::ShowRank(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
201 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"nameless tee - 01:40.00 - better than 100% - requested by brainless tee", "Global rank 1"}, All: true);
202}
203
204TEST_P(SingleScore, LoadPlayerData)
205{
206 InsertRank(Time: 120.0, WithTimeCheckPoints: true);
207 str_copy(dst: m_PlayerRequest.m_aName, src: "", dst_size: sizeof(m_PlayerRequest.m_aRequestingPlayer));
208 ASSERT_TRUE(CScoreWorker::LoadPlayerData(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
209
210 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::PLAYER_INFO);
211 ASSERT_FALSE(m_pPlayerResult->m_Data.m_Info.m_Time.has_value());
212 for(auto &Time : m_pPlayerResult->m_Data.m_Info.m_aTimeCp)
213 {
214 ASSERT_EQ(Time, 0);
215 }
216
217 str_copy(dst: m_PlayerRequest.m_aRequestingPlayer, src: "nameless tee", dst_size: sizeof(m_PlayerRequest.m_aRequestingPlayer));
218 str_copy(dst: m_PlayerRequest.m_aName, src: "", dst_size: sizeof(m_PlayerRequest.m_aRequestingPlayer));
219 ASSERT_TRUE(CScoreWorker::LoadPlayerData(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
220
221 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::PLAYER_INFO);
222 ASSERT_TRUE(m_pPlayerResult->m_Data.m_Info.m_Time.has_value());
223 ASSERT_EQ(*m_pPlayerResult->m_Data.m_Info.m_Time, 100.0);
224 for(int i = 0; i < NUM_CHECKPOINTS; i++)
225 {
226 ASSERT_EQ(m_pPlayerResult->m_Data.m_Info.m_aTimeCp[i], i);
227 }
228
229 str_copy(dst: m_PlayerRequest.m_aRequestingPlayer, src: "finishless", dst_size: sizeof(m_PlayerRequest.m_aRequestingPlayer));
230 str_copy(dst: m_PlayerRequest.m_aName, src: "nameless tee", dst_size: sizeof(m_PlayerRequest.m_aRequestingPlayer));
231 ASSERT_TRUE(CScoreWorker::LoadPlayerData(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
232
233 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::PLAYER_INFO);
234 ASSERT_FALSE(m_pPlayerResult->m_Data.m_Info.m_Time.has_value());
235 for(int i = 0; i < NUM_CHECKPOINTS; i++)
236 {
237 ASSERT_EQ(m_pPlayerResult->m_Data.m_Info.m_aTimeCp[i], i);
238 }
239}
240
241TEST_P(SingleScore, TimesExists)
242{
243 ASSERT_TRUE(CScoreWorker::ShowTimes(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
244 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::DIRECT);
245 EXPECT_STREQ(m_pPlayerResult->m_Data.m_aaMessages[0], "------------- Last Times -------------");
246 char aBuf[128];
247 str_copy(dst: aBuf, src: m_pPlayerResult->m_Data.m_aaMessages[1], dst_size: 7);
248 EXPECT_STREQ(aBuf, "[USA] ");
249
250 str_copy(dst: aBuf, src: m_pPlayerResult->m_Data.m_aaMessages[1] + str_length(str: m_pPlayerResult->m_Data.m_aaMessages[1]) - 10, dst_size: 11);
251 EXPECT_STREQ(aBuf, ", 01:40.00");
252 EXPECT_STREQ(m_pPlayerResult->m_Data.m_aaMessages[2], "-------------------------------------------");
253 for(int i = 3; i < CScorePlayerResult::MAX_MESSAGES; i++)
254 {
255 EXPECT_STREQ(m_pPlayerResult->m_Data.m_aaMessages[i], "");
256 }
257}
258
259TEST_P(SingleScore, TimesDoesntExist)
260{
261 str_copy(dst: m_PlayerRequest.m_aName, src: "foo", dst_size: sizeof(m_PlayerRequest.m_aMap));
262 ASSERT_TRUE(CScoreWorker::ShowTimes(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
263 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"There are no times in the specified range"});
264}
265
266struct TeamScore : public Score
267{
268 void SetUp() override
269 {
270 InsertTeamRank(Time: 100.0);
271 }
272
273 void InsertTeamRank(float Time = 100.0)
274 {
275 str_copy(dst: g_Config.m_SvSqlServerName, src: "USA", dst_size: sizeof(g_Config.m_SvSqlServerName));
276 CSqlTeamScoreData teamScoreData;
277 CSqlScoreData ScoreData(std::make_shared<CScorePlayerResult>());
278 str_copy(dst: teamScoreData.m_aMap, src: "Kobra 3", dst_size: sizeof(teamScoreData.m_aMap));
279 str_copy(dst: ScoreData.m_aMap, src: "Kobra 3", dst_size: sizeof(ScoreData.m_aMap));
280 str_copy(dst: teamScoreData.m_aGameUuid, src: "8d300ecf-5873-4297-bee5-95668fdff320", dst_size: sizeof(teamScoreData.m_aGameUuid));
281 str_copy(dst: ScoreData.m_aGameUuid, src: "8d300ecf-5873-4297-bee5-95668fdff320", dst_size: sizeof(ScoreData.m_aGameUuid));
282 teamScoreData.m_Size = 2;
283 str_copy(dst: teamScoreData.m_aaNames[0], src: "nameless tee", dst_size: sizeof(teamScoreData.m_aaNames[0]));
284 str_copy(dst: teamScoreData.m_aaNames[1], src: "brainless tee", dst_size: sizeof(teamScoreData.m_aaNames[1]));
285 teamScoreData.m_Time = Time;
286 ScoreData.m_Time = Time;
287 str_copy(dst: teamScoreData.m_aTimestamp, src: "2021-11-24 19:24:08", dst_size: sizeof(teamScoreData.m_aTimestamp));
288 str_copy(dst: ScoreData.m_aTimestamp, src: "2021-11-24 19:24:08", dst_size: sizeof(ScoreData.m_aTimestamp));
289 for(int i = 0; i < NUM_CHECKPOINTS; i++)
290 ScoreData.m_aCurrentTimeCp[i] = 0;
291 ASSERT_TRUE(CScoreWorker::SaveTeamScore(m_pConn, &teamScoreData, Write::NORMAL, m_aError, sizeof(m_aError))) << m_aError;
292
293 str_copy(dst: m_PlayerRequest.m_aMap, src: "Kobra 3", dst_size: sizeof(m_PlayerRequest.m_aMap));
294 str_copy(dst: m_PlayerRequest.m_aRequestingPlayer, src: "brainless tee", dst_size: sizeof(m_PlayerRequest.m_aRequestingPlayer));
295 str_copy(dst: ScoreData.m_aRequestingPlayer, src: "brainless tee", dst_size: sizeof(ScoreData.m_aRequestingPlayer));
296
297 str_copy(dst: ScoreData.m_aName, src: "nameless tee", dst_size: sizeof(ScoreData.m_aName));
298 ScoreData.m_ClientId = 0;
299 ASSERT_TRUE(CScoreWorker::SaveScore(m_pConn, &ScoreData, Write::NORMAL, m_aError, sizeof(m_aError))) << m_aError;
300 str_copy(dst: ScoreData.m_aName, src: "brainless tee", dst_size: sizeof(ScoreData.m_aName));
301 ScoreData.m_ClientId = 1;
302 ASSERT_TRUE(CScoreWorker::SaveScore(m_pConn, &ScoreData, Write::NORMAL, m_aError, sizeof(m_aError))) << m_aError;
303 m_PlayerRequest.m_Offset = 0;
304 }
305};
306
307TEST_P(TeamScore, All)
308{
309 g_Config.m_SvRegionalRankings = false;
310 ASSERT_TRUE(CScoreWorker::ShowTeamTop5(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
311 ExpectLines(pPlayerResult: m_pPlayerResult,
312 Lines: {"------- Team Top 5 -------",
313 "1. brainless tee & nameless tee Team Time: 01:40.00",
314 "-------------------------------"});
315}
316
317TEST_P(TeamScore, TeamTop5Regional)
318{
319 g_Config.m_SvRegionalRankings = true;
320 str_copy(dst: m_PlayerRequest.m_aServer, src: "USA", dst_size: sizeof(m_PlayerRequest.m_aServer));
321 ASSERT_TRUE(CScoreWorker::ShowTeamTop5(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
322 ExpectLines(pPlayerResult: m_pPlayerResult,
323 Lines: {"------- Team Top 5 -------",
324 "1. brainless tee & nameless tee Team Time: 01:40.00",
325 "----- USA Team Top -----",
326 "1. brainless tee & nameless tee Team Time: 01:40.00"});
327}
328
329TEST_P(TeamScore, PlayerExists)
330{
331 str_copy(dst: m_PlayerRequest.m_aName, src: "brainless tee", dst_size: sizeof(m_PlayerRequest.m_aName));
332 ASSERT_TRUE(CScoreWorker::ShowPlayerTeamTop5(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
333 ExpectLines(pPlayerResult: m_pPlayerResult,
334 Lines: {"------- Team Top 5 -------",
335 "1. brainless tee & nameless tee Team Time: 01:40.00",
336 "---------------------------------"});
337}
338
339TEST_P(TeamScore, PlayerDoesntExist)
340{
341 str_copy(dst: m_PlayerRequest.m_aName, src: "foo", dst_size: sizeof(m_PlayerRequest.m_aName));
342 ASSERT_TRUE(CScoreWorker::ShowPlayerTeamTop5(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
343 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"foo has no team ranks"});
344}
345
346TEST_P(TeamScore, RankUpdates)
347{
348 InsertTeamRank(Time: 98.0);
349 str_copy(dst: m_PlayerRequest.m_aName, src: "brainless tee", dst_size: sizeof(m_PlayerRequest.m_aName));
350 ASSERT_TRUE(CScoreWorker::ShowPlayerTeamTop5(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
351 ExpectLines(pPlayerResult: m_pPlayerResult,
352 Lines: {"------- Team Top 5 -------",
353 "1. brainless tee & nameless tee Team Time: 01:38.00",
354 "---------------------------------"});
355}
356
357struct MapInfo : public Score
358{
359 MapInfo()
360 {
361 str_copy(dst: m_PlayerRequest.m_aRequestingPlayer, src: "brainless tee", dst_size: sizeof(m_PlayerRequest.m_aRequestingPlayer));
362 }
363};
364
365TEST_P(MapInfo, ExactNoFinish)
366{
367 str_copy(dst: m_PlayerRequest.m_aName, src: "Kobra 3", dst_size: sizeof(m_PlayerRequest.m_aName));
368 ASSERT_TRUE(CScoreWorker::MapInfo(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
369
370 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::DIRECT);
371 EXPECT_THAT(m_pPlayerResult->m_Data.m_aaMessages[0], testing::MatchesRegex("\"Kobra 3\" by Zerodin on Novice, ★★★★★, 5 points, released .* ago, 0 finishes by 0 tees"));
372 for(int i = 1; i < CScorePlayerResult::MAX_MESSAGES; i++)
373 {
374 EXPECT_STREQ(m_pPlayerResult->m_Data.m_aaMessages[i], "");
375 }
376}
377
378TEST_P(MapInfo, ExactFinish)
379{
380 InsertRank();
381 str_copy(dst: m_PlayerRequest.m_aName, src: "Kobra 3", dst_size: sizeof(m_PlayerRequest.m_aName));
382 ASSERT_TRUE(CScoreWorker::MapInfo(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
383
384 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::DIRECT);
385 EXPECT_THAT(m_pPlayerResult->m_Data.m_aaMessages[0], testing::MatchesRegex("\"Kobra 3\" by Zerodin on Novice, ★★★★★, 5 points, released .* ago, 1 finish by 1 tee in 01:40 median"));
386 for(int i = 1; i < CScorePlayerResult::MAX_MESSAGES; i++)
387 {
388 EXPECT_STREQ(m_pPlayerResult->m_Data.m_aaMessages[i], "");
389 }
390}
391
392TEST_P(MapInfo, Fuzzy)
393{
394 InsertRank();
395 str_copy(dst: m_PlayerRequest.m_aName, src: "k3", dst_size: sizeof(m_PlayerRequest.m_aName));
396 ASSERT_TRUE(CScoreWorker::MapInfo(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
397
398 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::DIRECT);
399 EXPECT_THAT(m_pPlayerResult->m_Data.m_aaMessages[0], testing::MatchesRegex("\"Kobra 3\" by Zerodin on Novice, ★★★★★, 5 points, released .* ago, 1 finish by 1 tee in 01:40 median"));
400 for(int i = 1; i < CScorePlayerResult::MAX_MESSAGES; i++)
401 {
402 EXPECT_STREQ(m_pPlayerResult->m_Data.m_aaMessages[i], "");
403 }
404}
405
406TEST_P(MapInfo, FuzzyCase)
407{
408 InsertMap(pName: "Reflect", pMapper: "DarkOort", pServer: "Dummy", Points: 20, Stars: 3);
409 InsertMap(pName: "reflects", pMapper: "Ninjed & Pipou", pServer: "Solo", Points: 16, Stars: 4);
410 str_copy(dst: m_PlayerRequest.m_aName, src: "reflect", dst_size: sizeof(m_PlayerRequest.m_aName));
411 ASSERT_TRUE(CScoreWorker::MapInfo(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
412
413 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::DIRECT);
414 EXPECT_THAT(m_pPlayerResult->m_Data.m_aaMessages[0], testing::MatchesRegex("\"Reflect\" by DarkOort on Dummy, ★★★✰✰, 20 points, released .* ago, 0 finishes by 0 tees"));
415 for(int i = 1; i < CScorePlayerResult::MAX_MESSAGES; i++)
416 {
417 EXPECT_STREQ(m_pPlayerResult->m_Data.m_aaMessages[i], "");
418 }
419}
420
421TEST_P(MapInfo, DoesntExit)
422{
423 str_copy(dst: m_PlayerRequest.m_aName, src: "f", dst_size: sizeof(m_PlayerRequest.m_aName));
424 ASSERT_TRUE(CScoreWorker::MapInfo(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
425 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"No map like \"f\" found."});
426}
427
428struct MapVote : public Score
429{
430 MapVote()
431 {
432 str_copy(dst: m_PlayerRequest.m_aRequestingPlayer, src: "brainless tee", dst_size: sizeof(m_PlayerRequest.m_aRequestingPlayer));
433 }
434};
435
436TEST_P(MapVote, Exact)
437{
438 str_copy(dst: m_PlayerRequest.m_aName, src: "Kobra 3", dst_size: sizeof(m_PlayerRequest.m_aName));
439 ASSERT_TRUE(CScoreWorker::MapVote(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
440 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::MAP_VOTE);
441 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aMap, "Kobra 3");
442 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aReason, "/map");
443 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aServer, "novice");
444}
445
446TEST_P(MapVote, Fuzzy)
447{
448 str_copy(dst: m_PlayerRequest.m_aName, src: "k3", dst_size: sizeof(m_PlayerRequest.m_aName));
449 ASSERT_TRUE(CScoreWorker::MapVote(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
450 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::MAP_VOTE);
451 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aMap, "Kobra 3");
452 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aReason, "/map");
453 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aServer, "novice");
454}
455
456TEST_P(MapVote, FuzzyCase)
457{
458 InsertMap(pName: "Reflect", pMapper: "DarkOort", pServer: "Dummy", Points: 20, Stars: 3);
459 InsertMap(pName: "reflects", pMapper: "Ninjed & Pipou", pServer: "Solo", Points: 16, Stars: 4);
460 str_copy(dst: m_PlayerRequest.m_aName, src: "reflect", dst_size: sizeof(m_PlayerRequest.m_aName));
461 ASSERT_TRUE(CScoreWorker::MapVote(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
462 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::MAP_VOTE);
463 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aMap, "Reflect");
464 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aReason, "/map");
465 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aServer, "dummy");
466}
467
468TEST_P(MapVote, DoesntExist)
469{
470 str_copy(dst: m_PlayerRequest.m_aName, src: "f", dst_size: sizeof(m_PlayerRequest.m_aName));
471 ASSERT_TRUE(CScoreWorker::MapVote(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
472 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"No map like \"f\" found. Try adding a '%' at the start if you don't know the first character. Example: /map %castle for \"Out of Castle\""});
473}
474
475struct Points : public Score
476{
477 Points()
478 {
479 str_copy(dst: m_PlayerRequest.m_aName, src: "nameless tee", dst_size: sizeof(m_PlayerRequest.m_aName));
480 str_copy(dst: m_PlayerRequest.m_aRequestingPlayer, src: "brainless tee", dst_size: sizeof(m_PlayerRequest.m_aRequestingPlayer));
481 m_PlayerRequest.m_Offset = 0;
482 }
483};
484
485TEST_P(Points, NoPoints)
486{
487 ASSERT_TRUE(CScoreWorker::ShowPoints(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
488 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"nameless tee has not collected any points so far"});
489}
490
491TEST_P(Points, NoPointsTop)
492{
493 ASSERT_TRUE(CScoreWorker::ShowTopPoints(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
494 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"-------- Top Points --------",
495 "-------------------------------"});
496}
497
498TEST_P(Points, OnePoints)
499{
500 m_pConn->AddPoints(pPlayer: "nameless tee", Points: 2, pError: m_aError, ErrorSize: sizeof(m_aError));
501 ASSERT_TRUE(CScoreWorker::ShowPoints(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
502 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"1. nameless tee Points: 2, requested by brainless tee"}, All: true);
503}
504
505TEST_P(Points, OnePointsTop)
506{
507 m_pConn->AddPoints(pPlayer: "nameless tee", Points: 2, pError: m_aError, ErrorSize: sizeof(m_aError));
508 ASSERT_TRUE(CScoreWorker::ShowTopPoints(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
509 ExpectLines(pPlayerResult: m_pPlayerResult,
510 Lines: {"-------- Top Points --------",
511 "1. nameless tee Points: 2",
512 "-------------------------------"});
513}
514
515TEST_P(Points, TwoPoints)
516{
517 m_pConn->AddPoints(pPlayer: "nameless tee", Points: 2, pError: m_aError, ErrorSize: sizeof(m_aError));
518 m_pConn->AddPoints(pPlayer: "brainless tee", Points: 3, pError: m_aError, ErrorSize: sizeof(m_aError));
519 ASSERT_TRUE(CScoreWorker::ShowPoints(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
520 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"2. nameless tee Points: 2, requested by brainless tee"}, All: true);
521}
522
523TEST_P(Points, TwoPointsTop)
524{
525 m_pConn->AddPoints(pPlayer: "nameless tee", Points: 2, pError: m_aError, ErrorSize: sizeof(m_aError));
526 m_pConn->AddPoints(pPlayer: "brainless tee", Points: 3, pError: m_aError, ErrorSize: sizeof(m_aError));
527 ASSERT_TRUE(CScoreWorker::ShowTopPoints(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
528 ExpectLines(pPlayerResult: m_pPlayerResult,
529 Lines: {"-------- Top Points --------",
530 "1. brainless tee Points: 3",
531 "2. nameless tee Points: 2",
532 "-------------------------------"});
533}
534
535TEST_P(Points, EqualPoints)
536{
537 m_pConn->AddPoints(pPlayer: "nameless tee", Points: 2, pError: m_aError, ErrorSize: sizeof(m_aError));
538 m_pConn->AddPoints(pPlayer: "brainless tee", Points: 3, pError: m_aError, ErrorSize: sizeof(m_aError));
539 m_pConn->AddPoints(pPlayer: "nameless tee", Points: 1, pError: m_aError, ErrorSize: sizeof(m_aError));
540 ASSERT_TRUE(CScoreWorker::ShowPoints(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
541 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"1. nameless tee Points: 3, requested by brainless tee"}, All: true);
542}
543
544TEST_P(Points, EqualPointsTop)
545{
546 m_pConn->AddPoints(pPlayer: "nameless tee", Points: 2, pError: m_aError, ErrorSize: sizeof(m_aError));
547 m_pConn->AddPoints(pPlayer: "brainless tee", Points: 3, pError: m_aError, ErrorSize: sizeof(m_aError));
548 m_pConn->AddPoints(pPlayer: "nameless tee", Points: 1, pError: m_aError, ErrorSize: sizeof(m_aError));
549 ASSERT_TRUE(CScoreWorker::ShowTopPoints(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
550 ExpectLines(pPlayerResult: m_pPlayerResult,
551 Lines: {"-------- Top Points --------",
552 "1. brainless tee Points: 3",
553 "1. nameless tee Points: 3",
554 "-------------------------------"});
555}
556
557struct RandomMap : public Score
558{
559 std::shared_ptr<CScoreRandomMapResult> m_pRandomMapResult{std::make_shared<CScoreRandomMapResult>(args: 0)};
560 CSqlRandomMapRequest m_RandomMapRequest{m_pRandomMapResult};
561
562 RandomMap()
563 {
564 str_copy(dst: m_RandomMapRequest.m_aServerType, src: "Novice", dst_size: sizeof(m_RandomMapRequest.m_aServerType));
565 str_copy(dst: m_RandomMapRequest.m_aCurrentMap, src: "Kobra 4", dst_size: sizeof(m_RandomMapRequest.m_aCurrentMap));
566 str_copy(dst: m_RandomMapRequest.m_aRequestingPlayer, src: "nameless tee", dst_size: sizeof(m_RandomMapRequest.m_aRequestingPlayer));
567 }
568};
569
570TEST_P(RandomMap, NoStars)
571{
572 m_RandomMapRequest.m_MinStars = -1;
573 m_RandomMapRequest.m_MaxStars = -1;
574 ASSERT_TRUE(CScoreWorker::RandomMap(m_pConn, &m_RandomMapRequest, m_aError, sizeof(m_aError))) << m_aError;
575 EXPECT_EQ(m_pRandomMapResult->m_ClientId, 0);
576 EXPECT_STREQ(m_pRandomMapResult->m_aMap, "Kobra 3");
577 EXPECT_STREQ(m_pRandomMapResult->m_aMessage, "");
578}
579
580TEST_P(RandomMap, StarsExists)
581{
582 m_RandomMapRequest.m_MinStars = 5;
583 m_RandomMapRequest.m_MaxStars = 5;
584 ASSERT_TRUE(CScoreWorker::RandomMap(m_pConn, &m_RandomMapRequest, m_aError, sizeof(m_aError))) << m_aError;
585 EXPECT_EQ(m_pRandomMapResult->m_ClientId, 0);
586 EXPECT_STREQ(m_pRandomMapResult->m_aMap, "Kobra 3");
587 EXPECT_STREQ(m_pRandomMapResult->m_aMessage, "");
588}
589
590TEST_P(RandomMap, StarsRangeExists)
591{
592 m_RandomMapRequest.m_MinStars = 1;
593 m_RandomMapRequest.m_MaxStars = 5;
594 ASSERT_TRUE(CScoreWorker::RandomMap(m_pConn, &m_RandomMapRequest, m_aError, sizeof(m_aError))) << m_aError;
595 EXPECT_EQ(m_pRandomMapResult->m_ClientId, 0);
596 EXPECT_STRNE(m_pRandomMapResult->m_aMap, "");
597 EXPECT_STREQ(m_pRandomMapResult->m_aMessage, "");
598}
599
600TEST_P(RandomMap, StarsDoesntExist)
601{
602 m_RandomMapRequest.m_MinStars = 3;
603 m_RandomMapRequest.m_MaxStars = 3;
604 ASSERT_TRUE(CScoreWorker::RandomMap(m_pConn, &m_RandomMapRequest, m_aError, sizeof(m_aError))) << m_aError;
605 EXPECT_EQ(m_pRandomMapResult->m_ClientId, 0);
606 EXPECT_STREQ(m_pRandomMapResult->m_aMap, "");
607 EXPECT_STREQ(m_pRandomMapResult->m_aMessage, "No maps found on this server!");
608}
609
610TEST_P(RandomMap, UnfinishedExists)
611{
612 m_RandomMapRequest.m_MinStars = -1;
613 m_RandomMapRequest.m_MaxStars = -1;
614 ASSERT_TRUE(CScoreWorker::RandomUnfinishedMap(m_pConn, &m_RandomMapRequest, m_aError, sizeof(m_aError))) << m_aError;
615 EXPECT_EQ(m_pRandomMapResult->m_ClientId, 0);
616 EXPECT_STREQ(m_pRandomMapResult->m_aMap, "Kobra 3");
617 EXPECT_STREQ(m_pRandomMapResult->m_aMessage, "");
618}
619
620TEST_P(RandomMap, UnfinishedDoesntExist)
621{
622 InsertRank();
623 ASSERT_TRUE(CScoreWorker::RandomUnfinishedMap(m_pConn, &m_RandomMapRequest, m_aError, sizeof(m_aError))) << m_aError;
624 EXPECT_EQ(m_pRandomMapResult->m_ClientId, 0);
625 EXPECT_STREQ(m_pRandomMapResult->m_aMap, "");
626 EXPECT_STREQ(m_pRandomMapResult->m_aMessage, "nameless tee has no more unfinished maps on this server!");
627}
628
629auto g_pSqliteConn = CreateSqliteConnection(pFilename: ":memory:", Setup: true);
630#if defined(CONF_TEST_MYSQL)
631CMysqlConfig gMysqlConfig{
632 .m_aDatabase: "ddnet", // database
633 .m_aPrefix: "record", // prefix
634 .m_aUser: "ddnet", // user
635 .m_aPass: "thebestpassword", // password
636 .m_aIp: "localhost", // ip
637 .m_aBindaddr: "", // bindaddr
638 .m_Port: 3306, // port
639 .m_Setup: true, // setup
640};
641auto g_pMysqlConn = CreateMysqlConnection(Config: gMysqlConfig);
642#endif
643
644auto g_TestValues{
645 testing::Values(
646#if defined(CONF_TEST_MYSQL)
647 v: g_pMysqlConn.get(),
648#endif
649 v: g_pSqliteConn.get())};
650
651#define INSTANTIATE(SUITE) \
652 INSTANTIATE_TEST_SUITE_P(Sql, SUITE, g_TestValues, \
653 [](const testing::TestParamInfo<Score::ParamType> &Info) { \
654 switch(Info.index) \
655 { \
656 case 0: return "SQLite"; \
657 case 1: return "MySQL"; \
658 default: return "Unknown"; \
659 } \
660 })
661
662INSTANTIATE(SingleScore);
663INSTANTIATE(TeamScore);
664INSTANTIATE(MapInfo);
665INSTANTIATE(MapVote);
666INSTANTIATE(Points);
667INSTANTIATE(RandomMap);
668