From c2c2f9b073a25e036cb5b71316b701295d891cfb Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Fri, 27 Oct 2023 09:21:56 -0600 Subject: [PATCH] add tests for dorequest?r=uploadachievement (#1906) --- app/Helpers/database/achievement.php | 177 ++-- app/Helpers/database/static.php | 25 +- app/Helpers/database/user-activity.php | 22 +- app/Helpers/database/user-permission.php | 15 +- app/Helpers/database/user.php | 15 +- app/Platform/EventServiceProvider.php | 2 + ...tchUpdateDeveloperContributionYieldJob.php | 20 +- .../DispatchUpdateGameMetricsJob.php | 5 + .../Feature/Connect/UploadAchievementTest.php | 904 ++++++++++++++++++ 9 files changed, 1014 insertions(+), 171 deletions(-) create mode 100644 tests/Feature/Connect/UploadAchievementTest.php diff --git a/app/Helpers/database/achievement.php b/app/Helpers/database/achievement.php index efb5d8f900..b5e7f1d0ca 100644 --- a/app/Helpers/database/achievement.php +++ b/app/Helpers/database/achievement.php @@ -205,11 +205,6 @@ function UploadNewAchievement( return false; } - $dbAuthor = $author; - $rawDesc = $desc; - $rawTitle = $title; - sanitize_sql_inputs($title, $desc, $mem, $progress, $progressMax, $progressFmt, $dbAuthor, $type); - $typeValue = ""; if ($type === null || trim($type) === '' || $type === 'not-given') { $typeValue = "NULL"; @@ -226,67 +221,84 @@ function UploadNewAchievement( return false; } - $query = " - INSERT INTO Achievements ( - ID, GameID, Title, Description, - MemAddr, Progress, ProgressMax, - ProgressFormat, Points, Flags, type, - Author, DateCreated, DateModified, - Updated, VotesPos, VotesNeg, - BadgeName, DisplayOrder, AssocVideo, - TrueRatio - ) - VALUES ( - NULL, '$gameID', '$title', '$desc', - '$mem', '$progress', '$progressMax', - '$progressFmt', $points, $flag, $typeValue, - '$dbAuthor', NOW(), NOW(), - NOW(), 0, 0, - '$badge', 0, NULL, - 0 - )"; - $db = getMysqliConnection(); - if (mysqli_query($db, $query) !== false) { - $idInOut = mysqli_insert_id($db); - postActivity($author, ActivityType::UploadAchievement, $idInOut); - - static_addnewachievement($idInOut); - addArticleComment( - "Server", - ArticleType::Achievement, - $idInOut, - "$author uploaded this achievement.", - $author - ); - - // uploaded new achievement - AchievementCreated::dispatch(Achievement::find($idInOut)); - - return true; - } + $achievement = new Achievement(); + $achievement->GameID = $gameID; + $achievement->Title = $title; + $achievement->Description = $desc; + $achievement->MemAddr = $mem; + $achievement->Points = $points; + $achievement->Flags = $flag; + $achievement->type = ($typeValue == 'NULL') ? null : $type; + $achievement->Author = $author; + $achievement->BadgeName = $badge; + + $achievement->save(); + $idInOut = $achievement->ID; + postActivity($author, ActivityType::UploadAchievement, $idInOut); + + static_addnewachievement($idInOut); + addArticleComment( + "Server", + ArticleType::Achievement, + $idInOut, + "$author uploaded this achievement.", + $author + ); + + // uploaded new achievement + AchievementCreated::dispatch($achievement); - // failed - return false; + return true; } + // Achievement being updated - $query = "SELECT Flags, type, MemAddr, Points, Title, Description, BadgeName, Author FROM Achievements WHERE ID='$idInOut'"; - $dbResult = s_mysql_query($query); - if ($dbResult !== false && mysqli_num_rows($dbResult) == 1) { - $data = mysqli_fetch_assoc($dbResult); + $achievement = Achievement::find($idInOut); + if ($achievement) { + $fields = []; + + $changingPoints = ($achievement->Points != $points); + if ($changingPoints) { + $achievement->Points = $points; + $fields[] = "points"; + } + + if ($achievement->BadgeName !== $badge) { + $achievement->BadgeName = $badge; + $fields[] = "badge"; + } + + if ($achievement->Title !== $title) { + $achievement->Title = $title; + $fields[] = "title"; + } + + if ($achievement->Description !== $desc) { + $achievement->Description = $desc; + $fields[] = "description"; + } + + $changingType = ($achievement->type != $type && $type !== 'not-given'); + if ($changingType) { + $achievement->type = $type; + $fields[] = "type"; + } - $changingAchSet = ($data['Flags'] != $flag); - $changingType = ($data['type'] != $type && $type !== 'not-given'); - $changingPoints = ($data['Points'] != $points); - $changingTitle = ($data['Title'] !== $rawTitle); - $changingDescription = ($data['Description'] !== $rawDesc); - $changingBadge = ($data['BadgeName'] !== $badge); - $changingLogic = ($data['MemAddr'] != $mem); + $changingLogic = ($achievement->MemAddr != $mem); + if ($changingLogic) { + $achievement->MemAddr = $mem; + $fields[] = "logic"; + } + + $changingAchSet = ($achievement->Flags != $flag); + if ($changingAchSet) { + $achievement->Flags = $flag; + } if ($flag === AchievementFlag::OfficialCore || $changingAchSet) { // If modifying core or changing achievement state // changing ach set detected; user is $author, permissions is $userPermissions, target set is $flag // Only allow jr. devs to modify core achievements if they are the author and not updating logic or state - if ($userPermissions < Permissions::Developer && ($changingLogic || $changingAchSet || $data['Author'] !== $author)) { + if ($userPermissions < Permissions::Developer && ($changingLogic || $changingAchSet || $achievement->Author !== $author)) { // Must be developer to modify core logic! $errorOut = "You must be a developer to perform this action! Please drop a message in the forums to apply."; @@ -296,42 +308,21 @@ function UploadNewAchievement( if ($flag === AchievementFlag::Unofficial) { // If modifying unofficial // Only allow jr. devs to modify unofficial if they are the author - if ($userPermissions == Permissions::JuniorDeveloper && $data['Author'] !== $author) { + if ($userPermissions == Permissions::JuniorDeveloper && $achievement->Author !== $author) { $errorOut = "You must be a developer to perform this action! Please drop a message in the forums to apply."; return false; } } - // `null` is a valid type value, so we use a different fallback value. - if ($type === 'not-given' && $data['type'] !== null) { - $typeValue = "'" . $data['type'] . "'"; - } - - $query = "UPDATE Achievements SET Title='$title', Description='$desc', Progress='$progress', ProgressMax='$progressMax', ProgressFormat='$progressFmt', MemAddr='$mem', Points=$points, Flags=$flag, type=$typeValue, DateModified=NOW(), Updated=NOW(), BadgeName='$badge' WHERE ID=$idInOut"; - - $db = getMysqliConnection(); - if (mysqli_query($db, $query) !== false) { - // if ($changingAchSet || $changingPoints) { - // // When changing achievement set, all existing achievements that rely on this should be purged. - // // $query = "DELETE FROM Awarded WHERE ID='$idInOut'"; - // // nah, that's a bit harsh... esp if you're changing something tiny like the badge!! - // - // // if (s_mysql_query($query) !== false) { - // // $rowsAffected = mysqli_affected_rows($db); - // // // great - // // } else { - // // //meh - // // } - // } + if ($achievement->isDirty()) { + $achievement->save(); static_setlastupdatedgame($gameID); static_setlastupdatedachievement($idInOut); postActivity($author, ActivityType::EditAchievement, $idInOut); - $achievement = Achievement::find($idInOut); - if ($changingAchSet) { if ($flag === AchievementFlag::OfficialCore) { addArticleComment( @@ -354,25 +345,6 @@ function UploadNewAchievement( } expireGameTopAchievers($gameID); } else { - $fields = []; - if ($changingPoints) { - $fields[] = "points"; - } - if ($changingBadge) { - $fields[] = "badge"; - } - if ($changingLogic) { - $fields[] = "logic"; - } - if ($changingTitle) { - $fields[] = "title"; - } - if ($changingDescription) { - $fields[] = "description"; - } - if ($changingType) { - $fields[] = "type"; - } $editString = implode(', ', $fields); if (!empty($editString)) { @@ -392,12 +364,9 @@ function UploadNewAchievement( if ($changingType) { AchievementTypeChanged::dispatch($achievement); } - - return true; } - log_sql_fail(); - return false; + return true; } return false; diff --git a/app/Helpers/database/static.php b/app/Helpers/database/static.php index 53e10dd1ce..c510592436 100644 --- a/app/Helpers/database/static.php +++ b/app/Helpers/database/static.php @@ -8,12 +8,9 @@ */ function static_addnewachievement(int $id): void { - $query = "UPDATE StaticData AS sd "; - $query .= "SET sd.NumAchievements=sd.NumAchievements+1, sd.LastCreatedAchievementID='$id'"; - $dbResult = s_mysql_query($query); - if (!$dbResult) { - log_sql_fail(); - } + $query = "UPDATE StaticData "; + $query .= "SET NumAchievements=NumAchievements+1, LastCreatedAchievementID=$id"; + legacyDbStatement($query); } /** @@ -113,12 +110,8 @@ function static_setlastearnedachievement(int $id, string $user, int $points): vo */ function static_setlastupdatedgame(int $id): void { - $query = "UPDATE StaticData AS sd "; - $query .= "SET sd.LastUpdatedGameID = '$id'"; - $dbResult = s_mysql_query($query); - if (!$dbResult) { - log_sql_fail(); - } + $query = "UPDATE StaticData SET LastUpdatedGameID = $id"; + legacyDbStatement($query); } /** @@ -126,10 +119,6 @@ function static_setlastupdatedgame(int $id): void */ function static_setlastupdatedachievement(int $id): void { - $query = "UPDATE StaticData AS sd "; - $query .= "SET sd.LastUpdatedAchievementID = '$id'"; - $dbResult = s_mysql_query($query); - if (!$dbResult) { - log_sql_fail(); - } + $query = "UPDATE StaticData SET LastUpdatedAchievementID = $id"; + legacyDbStatement($query); } diff --git a/app/Helpers/database/user-activity.php b/app/Helpers/database/user-activity.php index 7d6ed5a479..7119e61cc5 100644 --- a/app/Helpers/database/user-activity.php +++ b/app/Helpers/database/user-activity.php @@ -169,8 +169,6 @@ function addArticleComment( return false; } - sanitize_sql_inputs($commentPayload); - // Note: $user is the person who just made a comment. $userID = getUserIDFromUser($user); @@ -183,33 +181,27 @@ function addArticleComment( return true; } - // Replace all single quotes with double quotes (to work with MYSQL DB) - // $commentPayload = str_replace( "'", "''", $commentPayload ); - if (is_array($articleID)) { + $bindings = []; + $articleIDs = $articleID; $arrayCount = count($articleID); $count = 0; $query = "INSERT INTO Comment (ArticleType, ArticleID, UserID, Payload) VALUES"; foreach ($articleID as $id) { - $query .= "( $articleType, $id, $userID, '$commentPayload' )"; + $bindings['commentPayload' . $count] = $commentPayload; + $query .= "( $articleType, $id, $userID, :commentPayload$count )"; if (++$count !== $arrayCount) { $query .= ","; } } } else { - $query = "INSERT INTO Comment (ArticleType, ArticleID, UserID, Payload) VALUES( $articleType, $articleID, $userID, '$commentPayload' )"; + $query = "INSERT INTO Comment (ArticleType, ArticleID, UserID, Payload) VALUES( $articleType, $articleID, $userID, :commentPayload)"; + $bindings = ['commentPayload' => $commentPayload]; $articleIDs = [$articleID]; } - $db = getMysqliConnection(); - $dbResult = mysqli_query($db, $query); - - if (!$dbResult) { - log_sql_fail(); - - return false; - } + legacyDbStatement($query, $bindings); // Inform Subscribers of this comment: foreach ($articleIDs as $id) { diff --git a/app/Helpers/database/user-permission.php b/app/Helpers/database/user-permission.php index 70506b41a5..60d165068e 100644 --- a/app/Helpers/database/user-permission.php +++ b/app/Helpers/database/user-permission.php @@ -9,19 +9,10 @@ function getUserPermissions(?string $user): int return 0; } - sanitize_sql_inputs($user); - - $query = "SELECT Permissions FROM UserAccounts WHERE User='$user'"; - $dbResult = s_mysql_query($query); - if (!$dbResult) { - log_sql_fail(); - - return 0; - } - - $data = mysqli_fetch_assoc($dbResult); + $query = "SELECT Permissions FROM UserAccounts WHERE User=:user"; + $row = legacyDbFetch($query, ['user' => $user]); - return (int) $data['Permissions']; + return $row ? (int) $row['Permissions'] : Permissions::Unregistered; } function SetAccountPermissionsJSON( diff --git a/app/Helpers/database/user.php b/app/Helpers/database/user.php index 814cea4b62..59f2b59cb2 100644 --- a/app/Helpers/database/user.php +++ b/app/Helpers/database/user.php @@ -45,19 +45,10 @@ function getUserIDFromUser(?string $user): int return 0; } - sanitize_sql_inputs($user); - - $query = "SELECT ID FROM UserAccounts WHERE User LIKE '$user'"; - $dbResult = s_mysql_query($query); - - if ($dbResult !== false) { - $data = mysqli_fetch_assoc($dbResult); - - return (int) ($data['ID'] ?? 0); - } + $query = "SELECT ID FROM UserAccounts WHERE User = :user"; + $row = legacyDbFetch($query, ['user' => $user]); - // cannot find user $user - return 0; + return $row ? (int) $row['ID'] : 0; } function getUserMetadataFromID(int $userID): ?array diff --git a/app/Platform/EventServiceProvider.php b/app/Platform/EventServiceProvider.php index 0b8b02b5d1..6efc0ef20f 100755 --- a/app/Platform/EventServiceProvider.php +++ b/app/Platform/EventServiceProvider.php @@ -22,6 +22,7 @@ use App\Platform\Events\PlayerMetricsUpdated; use App\Platform\Events\PlayerRankedStatusChanged; use App\Platform\Events\PlayerSessionHeartbeat; +// use App\Platform\Listeners\DispatchUpdateDeveloperContributionYieldJob; use App\Platform\Listeners\DispatchUpdateGameMetricsJob; use App\Platform\Listeners\DispatchUpdatePlayerGameMetricsJob; use App\Platform\Listeners\DispatchUpdatePlayerMetricsJob; @@ -34,6 +35,7 @@ class EventServiceProvider extends ServiceProvider { protected $listen = [ AchievementCreated::class => [ + DispatchUpdateGameMetricsJob::class, // dispatches GameMetricsUpdated ], AchievementPublished::class => [ DispatchUpdateGameMetricsJob::class, // dispatches GameMetricsUpdated diff --git a/app/Platform/Listeners/DispatchUpdateDeveloperContributionYieldJob.php b/app/Platform/Listeners/DispatchUpdateDeveloperContributionYieldJob.php index e8433151ee..ab5b1a7241 100644 --- a/app/Platform/Listeners/DispatchUpdateDeveloperContributionYieldJob.php +++ b/app/Platform/Listeners/DispatchUpdateDeveloperContributionYieldJob.php @@ -17,16 +17,16 @@ public function handle(object $event): void $user = null; switch ($event::class) { - // TODO case AchievementPublished::class: - // $achievement = $event->achievement; - // $achievement->loadMissing('developer'); - // $user = $achievement->developer; - // break; - // TODO case AchievementUnpublished::class: - // $achievement = $event->achievement; - // $achievement->loadMissing('developer'); - // $user = $achievement->developer; - // break; + case AchievementPublished::class: + $achievement = $event->achievement; + $achievement->loadMissing('developer'); + $user = $achievement->developer; + break; + case AchievementUnpublished::class: + $achievement = $event->achievement; + $achievement->loadMissing('developer'); + $user = $achievement->developer; + break; case AchievementPointsChanged::class: $achievement = $event->achievement; $achievement->loadMissing('developer'); diff --git a/app/Platform/Listeners/DispatchUpdateGameMetricsJob.php b/app/Platform/Listeners/DispatchUpdateGameMetricsJob.php index af9edcb98f..8de9eeabf9 100644 --- a/app/Platform/Listeners/DispatchUpdateGameMetricsJob.php +++ b/app/Platform/Listeners/DispatchUpdateGameMetricsJob.php @@ -2,6 +2,7 @@ namespace App\Platform\Listeners; +use App\Platform\Events\AchievementCreated; use App\Platform\Events\AchievementPointsChanged; use App\Platform\Events\AchievementPublished; use App\Platform\Events\AchievementTypeChanged; @@ -35,6 +36,10 @@ public function handle(object $event): void $achievement = $event->achievement; $game = $achievement->game; break; + case AchievementCreated::class: + $achievement = $event->achievement; + $game = $achievement->game; + break; case PlayerGameMetricsUpdated::class: $game = $event->game; break; diff --git a/tests/Feature/Connect/UploadAchievementTest.php b/tests/Feature/Connect/UploadAchievementTest.php new file mode 100644 index 0000000000..4c52d3b48a --- /dev/null +++ b/tests/Feature/Connect/UploadAchievementTest.php @@ -0,0 +1,904 @@ +create([ + 'Permissions' => Permissions::Developer, + 'appToken' => Str::random(16), + 'ContribCount' => 0, + 'ContribYield' => 0, + ]); + $game = $this->seedGame(withHash: false); + + /** @var Achievement $achievement1 */ + $achievement1 = Achievement::factory()->create(['GameID' => $game->ID + 1, 'Author' => $author->User]); + + AchievementSetClaim::factory()->create(['User' => $author->User, 'GameID' => $game->ID]); + + $params = [ + 'u' => $author->User, + 't' => $author->appToken, + 'g' => $game->ID, + 'n' => 'Title1', + 'd' => 'Description1', + 'z' => 5, + 'm' => '0xH0000=1', + 'f' => 5, // Unofficial - hardcode for test to prevent false success if enum changes + 'b' => '001234', + ]; + + // ==================================================== + // create an achievement + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID + 1, + 'Error' => '', + ]); + + /** @var Achievement $achievement2 */ + $achievement2 = Achievement::findOrFail($achievement1->ID + 1); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title1'); + $this->assertEquals($achievement2->MemAddr, '0xH0000=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertNull($achievement2->type); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertNull($achievement2->user_id); + $this->assertEquals($achievement2->BadgeName, '001234'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 0); + $this->assertEquals($game->achievements_unpublished, 1); + $this->assertEquals($game->points_total, 0); + + // ==================================================== + // publish achievement + $params['a'] = $achievement2->ID; + $params['f'] = 3; // Official - hardcode for test to prevent false success if enum changes + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title1'); + $this->assertEquals($achievement2->MemAddr, '0xH0000=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertNull($achievement2->type); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '001234'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 1); + $this->assertEquals($game->achievements_unpublished, 0); + $this->assertEquals($game->points_total, 5); + + // ==================================================== + // modify achievement + $params['n'] = 'Title2'; + $params['d'] = 'Description2'; + $params['z'] = 10; + $params['m'] = '0xH0001=1'; + $params['b'] = '002345'; + $params['x'] = 'progression'; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 1); + $this->assertEquals($game->achievements_unpublished, 0); + $this->assertEquals($game->points_total, 10); + + // ==================================================== + // unlock achievement; contrib yield changes + $this->addHardcoreUnlock($author, $achievement2); + $this->addHardcoreUnlock($this->user, $achievement2); + + $author->refresh(); + // When DispatchUpdateDeveloperContributionYieldJob is enabled, update all + // of the TODO blocks in this file to expect the contribution changes. + $this->assertEquals($author->ContribCount, 0); + /* TODO + $this->assertEquals($author->ContribCount, 1); + $this->assertEquals($author->ContribYield, 10); + */ + + $game->refresh(); + $this->assertEquals($game->players_total, 2); + $this->assertEquals($game->players_hardcore, 2); + + // ==================================================== + // rescore achievement; contrib yield changes + $params['z'] = 5; + unset($params['x']); // ommitting optional 'x' parameter should not change type + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 1); + $this->assertEquals($game->achievements_unpublished, 0); + $this->assertEquals($game->points_total, 5); + $this->assertEquals($game->players_total, 2); + $this->assertEquals($game->players_hardcore, 2); + + $author->refresh(); + /* TODO + $this->assertEquals($author->ContribCount, 1); + $this->assertEquals($author->ContribYield, 5); + */ + + // ==================================================== + // demote achievement; contrib yield changes + $params['f'] = 5; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 0); + $this->assertEquals($game->achievements_unpublished, 1); + $this->assertEquals($game->points_total, 0); + $this->assertEquals($game->players_total, 0); + $this->assertEquals($game->players_hardcore, 0); + + $author->refresh(); + $this->assertEquals($author->ContribCount, 0); + $this->assertEquals($author->ContribYield, 0); + + // ==================================================== + // change points while demoted + $params['z'] = 10; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 0); + $this->assertEquals($game->achievements_unpublished, 1); + $this->assertEquals($game->points_total, 0); + $this->assertEquals($game->players_total, 0); + $this->assertEquals($game->players_hardcore, 0); + + $author->refresh(); + $this->assertEquals($author->ContribCount, 0); + $this->assertEquals($author->ContribYield, 0); + + // ==================================================== + // repromote achievement; contrib yield changes + $params['f'] = 3; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 1); + $this->assertEquals($game->achievements_unpublished, 0); + $this->assertEquals($game->points_total, 10); + $this->assertEquals($game->players_total, 2); + $this->assertEquals($game->players_hardcore, 2); + + $author->refresh(); + /* TODO + $this->assertEquals($author->ContribCount, 1); + $this->assertEquals($author->ContribYield, 10); + */ + } + + public function testNonDevPermissions(): void + { + /** @var User $author */ + $author = User::factory()->create([ + 'Permissions' => Permissions::Registered, + 'appToken' => Str::random(16), + ]); + $game = $this->seedGame(withHash: false); + + /** @var Achievement $achievement1 */ + $achievement1 = Achievement::factory()->create(['GameID' => $game->ID, 'Author' => $author->User]); + + $params = [ + 'u' => $author->User, + 't' => $author->appToken, + 'g' => $game->ID, + 'n' => 'Title1', + 'd' => 'Description1', + 'z' => 5, + 'm' => '0xH0000=1', + 'f' => 5, // Unofficial - hardcode for test to prevent false success if enum changes + 'b' => '001234', + ]; + + // ==================================================== + // non-developer cannot create achievements + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => 0, + 'Error' => "You must be a developer to perform this action! Please drop a message in the forums to apply.", + ]); + } + + public function testJrDevPermissions(): void + { + /** @var User $author */ + $author = User::factory()->create([ + 'Permissions' => Permissions::JuniorDeveloper, + 'appToken' => Str::random(16), + ]); + $game = $this->seedGame(withHash: false); + + /** @var Achievement $achievement1 */ + $achievement1 = Achievement::factory()->create(['GameID' => $game->ID, 'Author' => $this->user->User]); + + $params = [ + 'u' => $author->User, + 't' => $author->appToken, + 'g' => $game->ID, + 'n' => 'Title1', + 'd' => 'Description1', + 'z' => 5, + 'm' => '0xH0000=1', + 'f' => 5, // Unofficial - hardcode for test to prevent false success if enum changes + 'b' => '001234', + ]; + + // ==================================================== + // junior developer cannot create achievement without claim + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => 0, + 'Error' => "You must have an active claim on this game to perform this action.", + ]); + + // ==================================================== + // junior developer can create achievement with claim + AchievementSetClaim::factory()->create(['User' => $author->User, 'GameID' => $game->ID]); + + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID + 1, + 'Error' => '', + ]); + + /** @var Achievement $achievement2 */ + $achievement2 = Achievement::findOrFail($achievement1->ID + 1); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title1'); + $this->assertEquals($achievement2->MemAddr, '0xH0000=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertNull($achievement2->type); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '001234'); + + // ==================================================== + // junior developer can modify their own achievement + $params['a'] = $achievement2->ID; + $params['n'] = 'Title2'; + $params['d'] = 'Description2'; + $params['z'] = 10; + $params['m'] = '0xH0001=1'; + $params['b'] = '002345'; + $params['x'] = 'progression'; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID + 1, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + // ==================================================== + // junior developer cannot modify an achievement owned by someone else + $params['a'] = $achievement1->ID; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => $achievement1->ID, + 'Error' => 'You must be a developer to perform this action! Please drop a message in the forums to apply.', + ]); + + $achievement1->refresh(); + $this->assertNotEquals($achievement1->Title, 'Title2'); + $this->assertNotEquals($achievement1->MemAddr, '0xH0001=1'); + $this->assertNotEquals($achievement1->Points, 10); + $this->assertEquals($achievement1->Flags, AchievementFlag::Unofficial); + $this->assertNotEquals($achievement1->type, 'progression'); + $this->assertNotEquals($achievement1->Author, $author->User); + $this->assertNotEquals($achievement1->BadgeName, '002345'); + + // ==================================================== + // junior developer cannot promote their own achievement + $params['a'] = $achievement2->ID; + $params['f'] = 3; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => $achievement2->ID, + 'Error' => 'You must be a developer to perform this action! Please drop a message in the forums to apply.', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + // ==================================================== + // junior developer cannot demote their own achievement + $achievement2->Flags = AchievementFlag::OfficialCore; + $achievement2->save(); + $params['f'] = 5; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => $achievement2->ID, + 'Error' => 'You must be a developer to perform this action! Please drop a message in the forums to apply.', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + // ==================================================== + // junior developer cannot change logic of their own achievement in core + $params['f'] = 3; + $params['m'] = '0xH0002=1'; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => $achievement2->ID, + 'Error' => 'You must be a developer to perform this action! Please drop a message in the forums to apply.', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + // ==================================================== + // junior developer can change all non-logic of their own achievement in core + $params['n'] = 'Title3'; + $params['d'] = 'Description3'; + $params['z'] = 5; + $params['m'] = '0xH0001=1'; + $params['b'] = '003456'; + $params['x'] = ''; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title3'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertNull($achievement2->type); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '003456'); + } + + public function testDevPermissions(): void + { + /** @var User $author */ + $author = User::factory()->create([ + 'Permissions' => Permissions::Developer, + 'appToken' => Str::random(16), + ]); + $game = $this->seedGame(withHash: false); + + /** @var Achievement $achievement1 */ + $achievement1 = Achievement::factory()->create(['GameID' => $game->ID, 'Author' => $this->user->User]); + + $params = [ + 'u' => $author->User, + 't' => $author->appToken, + 'g' => $game->ID, + 'n' => 'Title1', + 'd' => 'Description1', + 'z' => 5, + 'm' => '0xH0000=1', + 'f' => 5, // Unofficial - hardcode for test to prevent false success if enum changes + 'b' => '001234', + ]; + + // ==================================================== + // developer cannot create achievement without claim + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => 0, + 'Error' => "You must have an active claim on this game to perform this action.", + ]); + + // ==================================================== + // developer can create achievement with claim + AchievementSetClaim::factory()->create(['User' => $author->User, 'GameID' => $game->ID]); + + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID + 1, + 'Error' => '', + ]); + + /** @var Achievement $achievement2 */ + $achievement2 = Achievement::findOrFail($achievement1->ID + 1); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title1'); + $this->assertEquals($achievement2->MemAddr, '0xH0000=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertNull($achievement2->type); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '001234'); + + // ==================================================== + // developer can modify their own achievement + $params['a'] = $achievement2->ID; + $params['n'] = 'Title2'; + $params['d'] = 'Description2'; + $params['z'] = 10; + $params['m'] = '0xH0001=1'; + $params['b'] = '002345'; + $params['x'] = 'progression'; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID + 1, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + // ==================================================== + // developer can promote their own achievement + $params['f'] = 3; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + // ==================================================== + // developer can change all properties of their own achievement in core + $params['n'] = 'Title3'; + $params['d'] = 'Description3'; + $params['z'] = 5; + $params['m'] = '0xH0002=1'; + $params['b'] = '003456'; + $params['x'] = ''; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title3'); + $this->assertEquals($achievement2->MemAddr, '0xH0002=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertNull($achievement2->type); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '003456'); + + // ==================================================== + // developer can demote their own achievement + $params['f'] = 5; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title3'); + $this->assertEquals($achievement2->MemAddr, '0xH0002=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertNull($achievement2->type); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '003456'); + + // ==================================================== + // developer can modify an achievement owned by someone else + $params['a'] = $achievement1->ID; + $params['n'] = 'Title2'; + $params['d'] = 'Description2'; + $params['z'] = 10; + $params['m'] = '0xH0001=1'; + $params['b'] = '002345'; + $params['x'] = 'progression'; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID, + 'Error' => '', + ]); + + $achievement1->refresh(); + $this->assertEquals($achievement1->Title, 'Title2'); + $this->assertEquals($achievement1->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement1->Points, 10); + $this->assertEquals($achievement1->Flags, AchievementFlag::Unofficial); + $this->assertEquals($achievement1->type, 'progression'); + $this->assertEquals($achievement1->Author, $this->user->User); + $this->assertEquals($achievement1->BadgeName, '002345'); + + // ==================================================== + // developer can promote someone else's achievement + $params['f'] = 3; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID, + 'Error' => '', + ]); + + $achievement1->refresh(); + $this->assertEquals($achievement1->GameID, $game->ID); + $this->assertEquals($achievement1->Title, 'Title2'); + $this->assertEquals($achievement1->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement1->Points, 10); + $this->assertEquals($achievement1->Flags, AchievementFlag::OfficialCore); + $this->assertEquals($achievement1->type, 'progression'); + $this->assertEquals($achievement1->Author, $this->user->User); + $this->assertEquals($achievement1->BadgeName, '002345'); + + // ==================================================== + // developer can change all properties of someone else's achievement in core + $params['n'] = 'Title3'; + $params['d'] = 'Description3'; + $params['z'] = 5; + $params['m'] = '0xH0002=1'; + $params['b'] = '003456'; + $params['x'] = ''; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID, + 'Error' => '', + ]); + + $achievement1->refresh(); + $this->assertEquals($achievement1->GameID, $game->ID); + $this->assertEquals($achievement1->Title, 'Title3'); + $this->assertEquals($achievement1->MemAddr, '0xH0002=1'); + $this->assertEquals($achievement1->Points, 5); + $this->assertEquals($achievement1->Flags, AchievementFlag::OfficialCore); + $this->assertNull($achievement1->type); + $this->assertEquals($achievement1->Author, $this->user->User); + $this->assertEquals($achievement1->BadgeName, '003456'); + + // ==================================================== + // developer can demote someone else's achievement + $params['f'] = 5; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID, + 'Error' => '', + ]); + + $achievement1->refresh(); + $this->assertEquals($achievement1->GameID, $game->ID); + $this->assertEquals($achievement1->Title, 'Title3'); + $this->assertEquals($achievement1->MemAddr, '0xH0002=1'); + $this->assertEquals($achievement1->Points, 5); + $this->assertEquals($achievement1->Flags, AchievementFlag::Unofficial); + $this->assertNull($achievement1->type); + $this->assertEquals($achievement1->Author, $this->user->User); + $this->assertEquals($achievement1->BadgeName, '003456'); + } + + public function testRolloutConsole(): void + { + /** @var User $author */ + $author = User::factory()->create([ + 'Permissions' => Permissions::Developer, + 'appToken' => Str::random(16), + ]); + /** @var System $system */ + $system = System::factory()->create(['ID' => 500]); + $game = $this->seedGame(system: $system, withHash: false); + + AchievementSetClaim::factory()->create(['User' => $author->User, 'GameID' => $game->ID]); + + /** @var Achievement $achievement1 */ + $achievement1 = Achievement::factory()->create(['GameID' => $game->ID + 1, 'Author' => $author->User]); + + $params = [ + 'u' => $author->User, + 't' => $author->appToken, + 'g' => $game->ID, + 'n' => 'Title1', + 'd' => 'Description1', + 'z' => 5, + 'm' => '0xH0000=1', + 'f' => 5, + 'b' => '001234', + ]; + + // ==================================================== + // can upload to unofficial + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID + 1, + 'Error' => '', + ]); + + /** @var Achievement $achievement2 */ + $achievement2 = Achievement::findOrFail($achievement1->ID + 1); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title1'); + $this->assertEquals($achievement2->MemAddr, '0xH0000=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertNull($achievement2->type); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertNull($achievement2->user_id); + $this->assertEquals($achievement2->BadgeName, '001234'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 0); + $this->assertEquals($game->achievements_unpublished, 1); + $this->assertEquals($game->points_total, 0); + + // ==================================================== + // can modify in unofficial + $params['a'] = $achievement2->ID; + $params['n'] = 'Title2'; + $params['d'] = 'Description2'; + $params['z'] = 10; + $params['m'] = '0xH0001=1'; + $params['b'] = '002345'; + $params['x'] = 'progression'; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 0); + $this->assertEquals($game->achievements_unpublished, 1); + $this->assertEquals($game->points_total, 0); + + // ==================================================== + // cannot promote for rollout console + $params['f'] = 3; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => $achievement2->ID, + 'Error' => 'You cannot promote achievements for a game from an unsupported console (console ID: 500).', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + } + + public function testOtherErrors(): void + { + /** @var User $author */ + $author = User::factory()->create([ + 'Permissions' => Permissions::Developer, + 'appToken' => Str::random(16), + ]); + $game = $this->seedGame(withHash: false); + + AchievementSetClaim::factory()->create(['User' => $author->User, 'GameID' => $game->ID]); + + $params = [ + 'u' => $author->User, + 't' => $author->appToken, + 'g' => $game->ID, + 'n' => 'Title1', + 'd' => 'Description1', + 'z' => 5, + 'm' => '0xH0000=1', + 'f' => 5, + 'b' => '001234', + ]; + + // ==================================================== + // invalid flag + $params['f'] = 4; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => 0, + 'Error' => 'Invalid achievement flag', + ]); + + // ==================================================== + // invalid points + $params['f'] = 5; + $params['z'] = 15; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => 0, + 'Error' => 'Invalid points value (15).', + ]); + + // ==================================================== + // invalid type + $params['z'] = 10; + $params['x'] = 'unknown'; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => 0, + 'Error' => 'Invalid achievement type', + ]); + } +}