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 *> // NOLINT(readability-identifier-naming)
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() override
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");
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, const char *pName = "nameless tee")
80 {
81 str_copy(dst&: g_Config.m_SvSqlServerName, src: "USA");
82 CSqlScoreData ScoreData(std::make_shared<CScorePlayerResult>());
83 str_copy(dst&: ScoreData.m_aMap, src: "Kobra 3");
84 str_copy(dst&: ScoreData.m_aGameUuid, src: "8d300ecf-5873-4297-bee5-95668fdff320");
85 str_copy(dst&: ScoreData.m_aName, src: pName);
86 ScoreData.m_ClientId = 0;
87 ScoreData.m_Time = Time;
88 str_copy(dst&: ScoreData.m_aTimestamp, src: "2021-11-24 19:24:08");
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");
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 // NOLINT(readability-identifier-naming)
119{
120 SingleScore()
121 {
122 InsertRank();
123 str_copy(dst&: m_PlayerRequest.m_aMap, src: "Kobra 3");
124 str_copy(dst&: m_PlayerRequest.m_aRequestingPlayer, src: "brainless tee");
125 m_PlayerRequest.m_Offset = 0;
126 str_copy(dst&: m_PlayerRequest.m_aServer, src: "GER");
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");
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");
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");
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");
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, RankPercent)
205{
206 g_Config.m_SvRegionalRankings = false;
207 InsertRank(Time: 200.0, WithTimeCheckPoints: false, pName: "second tee");
208 InsertRank(Time: 300.0, WithTimeCheckPoints: false, pName: "third tee");
209 InsertRank(Time: 400.0, WithTimeCheckPoints: false, pName: "fourth tee");
210 str_copy(dst&: m_PlayerRequest.m_aName, src: "third tee");
211 ASSERT_TRUE(CScoreWorker::ShowRank(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
212 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"third tee - 05:00.00 - better than 33% - requested by brainless tee", "Global rank 3"}, All: true);
213}
214
215TEST_P(SingleScore, LoadPlayerData)
216{
217 InsertRank(Time: 120.0, WithTimeCheckPoints: true);
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_FALSE(m_pPlayerResult->m_Data.m_Info.m_Time.has_value());
223 for(auto &Time : m_pPlayerResult->m_Data.m_Info.m_aTimeCp)
224 {
225 ASSERT_EQ(Time, 0);
226 }
227
228 str_copy(dst&: m_PlayerRequest.m_aRequestingPlayer, src: "nameless tee");
229 str_copy(dst: m_PlayerRequest.m_aName, src: "", dst_size: sizeof(m_PlayerRequest.m_aRequestingPlayer));
230 ASSERT_TRUE(CScoreWorker::LoadPlayerData(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
231
232 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::PLAYER_INFO);
233 ASSERT_TRUE(m_pPlayerResult->m_Data.m_Info.m_Time.has_value());
234 ASSERT_EQ(*m_pPlayerResult->m_Data.m_Info.m_Time, 100.0);
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 str_copy(dst&: m_PlayerRequest.m_aRequestingPlayer, src: "finishless");
241 str_copy(dst: m_PlayerRequest.m_aName, src: "nameless tee", dst_size: sizeof(m_PlayerRequest.m_aRequestingPlayer));
242 ASSERT_TRUE(CScoreWorker::LoadPlayerData(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
243
244 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::PLAYER_INFO);
245 ASSERT_FALSE(m_pPlayerResult->m_Data.m_Info.m_Time.has_value());
246 for(int i = 0; i < NUM_CHECKPOINTS; i++)
247 {
248 ASSERT_EQ(m_pPlayerResult->m_Data.m_Info.m_aTimeCp[i], i);
249 }
250}
251
252TEST_P(SingleScore, TimesExists)
253{
254 ASSERT_TRUE(CScoreWorker::ShowTimes(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
255 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::DIRECT);
256 EXPECT_STREQ(m_pPlayerResult->m_Data.m_aaMessages[0], "------------- Last Times -------------");
257 char aBuf[128];
258 str_copy(dst: aBuf, src: m_pPlayerResult->m_Data.m_aaMessages[1], dst_size: 7);
259 EXPECT_STREQ(aBuf, "[USA] ");
260
261 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);
262 EXPECT_STREQ(aBuf, ", 01:40.00");
263 EXPECT_STREQ(m_pPlayerResult->m_Data.m_aaMessages[2], "-------------------------------------------");
264 for(int i = 3; i < CScorePlayerResult::MAX_MESSAGES; i++)
265 {
266 EXPECT_STREQ(m_pPlayerResult->m_Data.m_aaMessages[i], "");
267 }
268}
269
270TEST_P(SingleScore, TimesDoesntExist)
271{
272 str_copy(dst: m_PlayerRequest.m_aName, src: "foo", dst_size: sizeof(m_PlayerRequest.m_aMap));
273 ASSERT_TRUE(CScoreWorker::ShowTimes(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
274 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"There are no times in the specified range"});
275}
276
277struct TeamScore : public Score // NOLINT(readability-identifier-naming)
278{
279 void SetUp() override
280 {
281 InsertTeamRank(Time: 100.0);
282 }
283
284 void InsertTeamRank(float Time = 100.0)
285 {
286 str_copy(dst&: g_Config.m_SvSqlServerName, src: "USA");
287 CSqlTeamScoreData TeamScoreData;
288 CSqlScoreData ScoreData(std::make_shared<CScorePlayerResult>());
289 str_copy(dst&: TeamScoreData.m_aMap, src: "Kobra 3");
290 str_copy(dst&: ScoreData.m_aMap, src: "Kobra 3");
291 str_copy(dst&: TeamScoreData.m_aGameUuid, src: "8d300ecf-5873-4297-bee5-95668fdff320");
292 str_copy(dst&: ScoreData.m_aGameUuid, src: "8d300ecf-5873-4297-bee5-95668fdff320");
293 TeamScoreData.m_Size = 2;
294 str_copy(dst&: TeamScoreData.m_aaNames[0], src: "nameless tee");
295 str_copy(dst&: TeamScoreData.m_aaNames[1], src: "brainless tee");
296 TeamScoreData.m_Time = Time;
297 ScoreData.m_Time = Time;
298 str_copy(dst&: TeamScoreData.m_aTimestamp, src: "2021-11-24 19:24:08");
299 str_copy(dst&: ScoreData.m_aTimestamp, src: "2021-11-24 19:24:08");
300 std::fill(first: std::begin(arr&: ScoreData.m_aCurrentTimeCp), last: std::end(arr&: ScoreData.m_aCurrentTimeCp), value: 0);
301 ASSERT_TRUE(CScoreWorker::SaveTeamScore(m_pConn, &TeamScoreData, Write::NORMAL, m_aError, sizeof(m_aError))) << m_aError;
302
303 str_copy(dst&: m_PlayerRequest.m_aMap, src: "Kobra 3");
304 str_copy(dst&: m_PlayerRequest.m_aRequestingPlayer, src: "brainless tee");
305 str_copy(dst&: ScoreData.m_aRequestingPlayer, src: "brainless tee");
306
307 str_copy(dst&: ScoreData.m_aName, src: "nameless tee");
308 ScoreData.m_ClientId = 0;
309 ASSERT_TRUE(CScoreWorker::SaveScore(m_pConn, &ScoreData, Write::NORMAL, m_aError, sizeof(m_aError))) << m_aError;
310 str_copy(dst&: ScoreData.m_aName, src: "brainless tee");
311 ScoreData.m_ClientId = 1;
312 ASSERT_TRUE(CScoreWorker::SaveScore(m_pConn, &ScoreData, Write::NORMAL, m_aError, sizeof(m_aError))) << m_aError;
313 m_PlayerRequest.m_Offset = 0;
314 }
315};
316
317TEST_P(TeamScore, All)
318{
319 g_Config.m_SvRegionalRankings = false;
320 ASSERT_TRUE(CScoreWorker::ShowTeamTop5(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
321 ExpectLines(pPlayerResult: m_pPlayerResult,
322 Lines: {"------- Team Top 5 -------",
323 "1. brainless tee & nameless tee Team Time: 01:40.00",
324 "-------------------------------"});
325}
326
327TEST_P(TeamScore, TeamTop5Regional)
328{
329 g_Config.m_SvRegionalRankings = true;
330 str_copy(dst&: m_PlayerRequest.m_aServer, src: "USA");
331 ASSERT_TRUE(CScoreWorker::ShowTeamTop5(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
332 ExpectLines(pPlayerResult: m_pPlayerResult,
333 Lines: {"------- Team Top 5 -------",
334 "1. brainless tee & nameless tee Team Time: 01:40.00",
335 "----- USA Team Top -----",
336 "1. brainless tee & nameless tee Team Time: 01:40.00"});
337}
338
339TEST_P(TeamScore, PlayerExists)
340{
341 str_copy(dst&: m_PlayerRequest.m_aName, src: "brainless tee");
342 ASSERT_TRUE(CScoreWorker::ShowPlayerTeamTop5(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
343 ExpectLines(pPlayerResult: m_pPlayerResult,
344 Lines: {"------- Team Top 5 -------",
345 "1. brainless tee & nameless tee Team Time: 01:40.00",
346 "---------------------------------"});
347}
348
349TEST_P(TeamScore, PlayerDoesntExist)
350{
351 str_copy(dst&: m_PlayerRequest.m_aName, src: "foo");
352 ASSERT_TRUE(CScoreWorker::ShowPlayerTeamTop5(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
353 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"foo has no team ranks"});
354}
355
356TEST_P(TeamScore, RankUpdates)
357{
358 InsertTeamRank(Time: 98.0);
359 str_copy(dst&: m_PlayerRequest.m_aName, src: "brainless tee");
360 ASSERT_TRUE(CScoreWorker::ShowPlayerTeamTop5(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
361 ExpectLines(pPlayerResult: m_pPlayerResult,
362 Lines: {"------- Team Top 5 -------",
363 "1. brainless tee & nameless tee Team Time: 01:38.00",
364 "---------------------------------"});
365}
366
367struct MapInfo : public Score // NOLINT(readability-identifier-naming)
368{
369 MapInfo()
370 {
371 str_copy(dst&: m_PlayerRequest.m_aRequestingPlayer, src: "brainless tee");
372 }
373};
374
375TEST_P(MapInfo, ExactNoFinish)
376{
377 str_copy(dst&: m_PlayerRequest.m_aName, src: "Kobra 3");
378 ASSERT_TRUE(CScoreWorker::MapInfo(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
379
380 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::DIRECT);
381 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"));
382 for(int i = 1; i < CScorePlayerResult::MAX_MESSAGES; i++)
383 {
384 EXPECT_STREQ(m_pPlayerResult->m_Data.m_aaMessages[i], "");
385 }
386}
387
388TEST_P(MapInfo, ExactFinish)
389{
390 InsertRank(Time: 42.87f);
391 str_copy(dst&: m_PlayerRequest.m_aRequestingPlayer, src: "nameless tee");
392 str_copy(dst&: m_PlayerRequest.m_aName, src: "Kobra 3");
393 ASSERT_TRUE(CScoreWorker::MapInfo(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
394
395 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::DIRECT);
396 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 00:42 median, your time: 42\\.87"));
397 for(int i = 1; i < CScorePlayerResult::MAX_MESSAGES; i++)
398 {
399 EXPECT_STREQ(m_pPlayerResult->m_Data.m_aaMessages[i], "");
400 }
401}
402
403TEST_P(MapInfo, Fuzzy)
404{
405 InsertRank();
406 str_copy(dst&: m_PlayerRequest.m_aName, src: "k3");
407 ASSERT_TRUE(CScoreWorker::MapInfo(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
408
409 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::DIRECT);
410 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"));
411 for(int i = 1; i < CScorePlayerResult::MAX_MESSAGES; i++)
412 {
413 EXPECT_STREQ(m_pPlayerResult->m_Data.m_aaMessages[i], "");
414 }
415}
416
417TEST_P(MapInfo, FuzzyCase)
418{
419 InsertMap(pName: "Reflect", pMapper: "DarkOort", pServer: "Dummy", Points: 20, Stars: 3);
420 InsertMap(pName: "reflects", pMapper: "Ninjed & Pipou", pServer: "Solo", Points: 16, Stars: 4);
421 str_copy(dst&: m_PlayerRequest.m_aName, src: "reflect");
422 ASSERT_TRUE(CScoreWorker::MapInfo(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
423
424 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::DIRECT);
425 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"));
426 for(int i = 1; i < CScorePlayerResult::MAX_MESSAGES; i++)
427 {
428 EXPECT_STREQ(m_pPlayerResult->m_Data.m_aaMessages[i], "");
429 }
430}
431
432TEST_P(MapInfo, DoesntExit)
433{
434 str_copy(dst&: m_PlayerRequest.m_aName, src: "f");
435 ASSERT_TRUE(CScoreWorker::MapInfo(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
436 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"No map like \"f\" found."});
437}
438
439struct MapVote : public Score // NOLINT(readability-identifier-naming)
440{
441 MapVote()
442 {
443 str_copy(dst&: m_PlayerRequest.m_aRequestingPlayer, src: "brainless tee");
444 }
445};
446
447TEST_P(MapVote, Exact)
448{
449 str_copy(dst&: m_PlayerRequest.m_aName, src: "Kobra 3");
450 ASSERT_TRUE(CScoreWorker::MapVote(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
451 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::MAP_VOTE);
452 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aMap, "Kobra 3");
453 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aReason, "/map");
454 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aServer, "novice");
455}
456
457TEST_P(MapVote, Fuzzy)
458{
459 str_copy(dst&: m_PlayerRequest.m_aName, src: "k3");
460 ASSERT_TRUE(CScoreWorker::MapVote(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
461 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::MAP_VOTE);
462 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aMap, "Kobra 3");
463 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aReason, "/map");
464 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aServer, "novice");
465}
466
467TEST_P(MapVote, FuzzyCase)
468{
469 InsertMap(pName: "Reflect", pMapper: "DarkOort", pServer: "Dummy", Points: 20, Stars: 3);
470 InsertMap(pName: "reflects", pMapper: "Ninjed & Pipou", pServer: "Solo", Points: 16, Stars: 4);
471 str_copy(dst&: m_PlayerRequest.m_aName, src: "reflect");
472 ASSERT_TRUE(CScoreWorker::MapVote(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
473 EXPECT_EQ(m_pPlayerResult->m_MessageKind, CScorePlayerResult::MAP_VOTE);
474 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aMap, "Reflect");
475 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aReason, "/map");
476 EXPECT_STREQ(m_pPlayerResult->m_Data.m_MapVote.m_aServer, "dummy");
477}
478
479TEST_P(MapVote, DoesntExist)
480{
481 str_copy(dst&: m_PlayerRequest.m_aName, src: "f");
482 ASSERT_TRUE(CScoreWorker::MapVote(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
483 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\""});
484}
485
486struct Points : public Score // NOLINT(readability-identifier-naming)
487{
488 Points()
489 {
490 str_copy(dst&: m_PlayerRequest.m_aName, src: "nameless tee");
491 str_copy(dst&: m_PlayerRequest.m_aRequestingPlayer, src: "brainless tee");
492 m_PlayerRequest.m_Offset = 0;
493 }
494};
495
496TEST_P(Points, NoPoints)
497{
498 ASSERT_TRUE(CScoreWorker::ShowPoints(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
499 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"nameless tee has not collected any points so far"});
500}
501
502TEST_P(Points, NoPointsTop)
503{
504 ASSERT_TRUE(CScoreWorker::ShowTopPoints(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
505 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"-------- Top Points --------",
506 "-------------------------------"});
507}
508
509TEST_P(Points, OnePoints)
510{
511 m_pConn->AddPoints(pPlayer: "nameless tee", Points: 2, pError: m_aError, ErrorSize: sizeof(m_aError));
512 ASSERT_TRUE(CScoreWorker::ShowPoints(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
513 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"1. nameless tee Points: 2, requested by brainless tee"}, All: true);
514}
515
516TEST_P(Points, OnePointsTop)
517{
518 m_pConn->AddPoints(pPlayer: "nameless tee", Points: 2, pError: m_aError, ErrorSize: sizeof(m_aError));
519 ASSERT_TRUE(CScoreWorker::ShowTopPoints(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
520 ExpectLines(pPlayerResult: m_pPlayerResult,
521 Lines: {"-------- Top Points --------",
522 "1. nameless tee Points: 2",
523 "-------------------------------"});
524}
525
526TEST_P(Points, TwoPoints)
527{
528 m_pConn->AddPoints(pPlayer: "nameless tee", Points: 2, pError: m_aError, ErrorSize: sizeof(m_aError));
529 m_pConn->AddPoints(pPlayer: "brainless tee", Points: 3, pError: m_aError, ErrorSize: sizeof(m_aError));
530 ASSERT_TRUE(CScoreWorker::ShowPoints(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
531 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"2. nameless tee Points: 2, requested by brainless tee"}, All: true);
532}
533
534TEST_P(Points, TwoPointsTop)
535{
536 m_pConn->AddPoints(pPlayer: "nameless tee", Points: 2, pError: m_aError, ErrorSize: sizeof(m_aError));
537 m_pConn->AddPoints(pPlayer: "brainless tee", Points: 3, pError: m_aError, ErrorSize: sizeof(m_aError));
538 ASSERT_TRUE(CScoreWorker::ShowTopPoints(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
539 ExpectLines(pPlayerResult: m_pPlayerResult,
540 Lines: {"-------- Top Points --------",
541 "1. brainless tee Points: 3",
542 "2. nameless tee Points: 2",
543 "-------------------------------"});
544}
545
546TEST_P(Points, EqualPoints)
547{
548 m_pConn->AddPoints(pPlayer: "nameless tee", Points: 2, pError: m_aError, ErrorSize: sizeof(m_aError));
549 m_pConn->AddPoints(pPlayer: "brainless tee", Points: 3, pError: m_aError, ErrorSize: sizeof(m_aError));
550 m_pConn->AddPoints(pPlayer: "nameless tee", Points: 1, pError: m_aError, ErrorSize: sizeof(m_aError));
551 ASSERT_TRUE(CScoreWorker::ShowPoints(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
552 ExpectLines(pPlayerResult: m_pPlayerResult, Lines: {"1. nameless tee Points: 3, requested by brainless tee"}, All: true);
553}
554
555TEST_P(Points, EqualPointsTop)
556{
557 m_pConn->AddPoints(pPlayer: "nameless tee", Points: 2, pError: m_aError, ErrorSize: sizeof(m_aError));
558 m_pConn->AddPoints(pPlayer: "brainless tee", Points: 3, pError: m_aError, ErrorSize: sizeof(m_aError));
559 m_pConn->AddPoints(pPlayer: "nameless tee", Points: 1, pError: m_aError, ErrorSize: sizeof(m_aError));
560 ASSERT_TRUE(CScoreWorker::ShowTopPoints(m_pConn, &m_PlayerRequest, m_aError, sizeof(m_aError))) << m_aError;
561 ExpectLines(pPlayerResult: m_pPlayerResult,
562 Lines: {"-------- Top Points --------",
563 "1. brainless tee Points: 3",
564 "1. nameless tee Points: 3",
565 "-------------------------------"});
566}
567
568struct RandomMap : public Score // NOLINT(readability-identifier-naming)
569{
570 std::shared_ptr<CScoreRandomMapResult> m_pRandomMapResult{std::make_shared<CScoreRandomMapResult>(args: 0)};
571 CSqlRandomMapRequest m_RandomMapRequest{m_pRandomMapResult};
572
573 RandomMap()
574 {
575 str_copy(dst&: m_RandomMapRequest.m_aServerType, src: "Novice");
576 str_copy(dst&: m_RandomMapRequest.m_aCurrentMap, src: "Kobra 4");
577 str_copy(dst&: m_RandomMapRequest.m_aRequestingPlayer, src: "nameless tee");
578 }
579};
580
581TEST_P(RandomMap, NoStars)
582{
583 m_RandomMapRequest.m_MinStars = -1;
584 m_RandomMapRequest.m_MaxStars = -1;
585 ASSERT_TRUE(CScoreWorker::RandomMap(m_pConn, &m_RandomMapRequest, m_aError, sizeof(m_aError))) << m_aError;
586 EXPECT_EQ(m_pRandomMapResult->m_ClientId, 0);
587 EXPECT_STREQ(m_pRandomMapResult->m_aMap, "Kobra 3");
588 EXPECT_STREQ(m_pRandomMapResult->m_aMessage, "");
589}
590
591TEST_P(RandomMap, StarsExists)
592{
593 m_RandomMapRequest.m_MinStars = 5;
594 m_RandomMapRequest.m_MaxStars = 5;
595 ASSERT_TRUE(CScoreWorker::RandomMap(m_pConn, &m_RandomMapRequest, m_aError, sizeof(m_aError))) << m_aError;
596 EXPECT_EQ(m_pRandomMapResult->m_ClientId, 0);
597 EXPECT_STREQ(m_pRandomMapResult->m_aMap, "Kobra 3");
598 EXPECT_STREQ(m_pRandomMapResult->m_aMessage, "");
599}
600
601TEST_P(RandomMap, StarsRangeExists)
602{
603 m_RandomMapRequest.m_MinStars = 1;
604 m_RandomMapRequest.m_MaxStars = 5;
605 ASSERT_TRUE(CScoreWorker::RandomMap(m_pConn, &m_RandomMapRequest, m_aError, sizeof(m_aError))) << m_aError;
606 EXPECT_EQ(m_pRandomMapResult->m_ClientId, 0);
607 EXPECT_STRNE(m_pRandomMapResult->m_aMap, "");
608 EXPECT_STREQ(m_pRandomMapResult->m_aMessage, "");
609}
610
611TEST_P(RandomMap, StarsDoesntExist)
612{
613 m_RandomMapRequest.m_MinStars = 3;
614 m_RandomMapRequest.m_MaxStars = 3;
615 ASSERT_TRUE(CScoreWorker::RandomMap(m_pConn, &m_RandomMapRequest, m_aError, sizeof(m_aError))) << m_aError;
616 EXPECT_EQ(m_pRandomMapResult->m_ClientId, 0);
617 EXPECT_STREQ(m_pRandomMapResult->m_aMap, "");
618 EXPECT_STREQ(m_pRandomMapResult->m_aMessage, "No maps found on this server!");
619}
620
621TEST_P(RandomMap, UnfinishedExists)
622{
623 m_RandomMapRequest.m_MinStars = -1;
624 m_RandomMapRequest.m_MaxStars = -1;
625 ASSERT_TRUE(CScoreWorker::RandomUnfinishedMap(m_pConn, &m_RandomMapRequest, m_aError, sizeof(m_aError))) << m_aError;
626 EXPECT_EQ(m_pRandomMapResult->m_ClientId, 0);
627 EXPECT_STREQ(m_pRandomMapResult->m_aMap, "Kobra 3");
628 EXPECT_STREQ(m_pRandomMapResult->m_aMessage, "");
629}
630
631TEST_P(RandomMap, UnfinishedDoesntExist)
632{
633 InsertRank();
634 ASSERT_TRUE(CScoreWorker::RandomUnfinishedMap(m_pConn, &m_RandomMapRequest, m_aError, sizeof(m_aError))) << m_aError;
635 EXPECT_EQ(m_pRandomMapResult->m_ClientId, 0);
636 EXPECT_STREQ(m_pRandomMapResult->m_aMap, "");
637 EXPECT_STREQ(m_pRandomMapResult->m_aMessage, "nameless tee has no more unfinished maps on this server!");
638}
639
640static auto g_pSqliteConn = CreateSqliteConnection(pFilename: ":memory:", Setup: true);
641#if defined(CONF_TEST_MYSQL)
642CMysqlConfig gMysqlConfig{
643 .m_aDatabase: "ddnet", // database
644 .m_aPrefix: "record", // prefix
645 .m_aUser: "ddnet", // user
646 .m_aPass: "thebestpassword", // password
647 .m_aIp: "localhost", // ip
648 .m_aBindaddr: "", // bindaddr
649 .m_Port: 3306, // port
650 .m_Setup: true, // setup
651};
652static auto g_pMysqlConn = CreateMysqlConnection(Config: gMysqlConfig);
653#endif
654
655static auto g_TestValues{
656 testing::Values(
657#if defined(CONF_TEST_MYSQL)
658 v: g_pMysqlConn.get(),
659#endif
660 v: g_pSqliteConn.get())};
661
662#define INSTANTIATE(SUITE) \
663 INSTANTIATE_TEST_SUITE_P(Sql, SUITE, g_TestValues, \
664 [](const testing::TestParamInfo<Score::ParamType> &Info) { \
665 switch(Info.index) \
666 { \
667 case 0: return "SQLite"; \
668 case 1: return "MySQL"; \
669 default: return "Unknown"; \
670 } \
671 })
672
673INSTANTIATE(SingleScore);
674INSTANTIATE(TeamScore);
675INSTANTIATE(MapInfo);
676INSTANTIATE(MapVote);
677INSTANTIATE(Points);
678INSTANTIATE(RandomMap);
679