Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b29f42c
feat(api): get set request list for user
drisc Dec 17, 2024
345a357
feat(api): add new factory and test
drisc Dec 18, 2024
6e9057b
fix: added HasFactory to model
drisc Dec 18, 2024
41c7aa4
chore: lint
drisc Dec 18, 2024
882911a
fix: composer analyse issues
drisc Dec 18, 2024
15c0c7d
fix: lint
drisc Dec 18, 2024
ced6d8c
Merge branch 'RetroAchievements:master' into setrequest-api
drisc Dec 18, 2024
235d470
fix: lift queries out of forloop
drisc Dec 18, 2024
967c9e1
nit: hardcoded assert values based on feedback
drisc Dec 19, 2024
5dd2a23
fix(style): moved newFactory() after $casts
drisc Dec 19, 2024
4d42e8b
nit: added default value for type to usergamelistentry factory
drisc Dec 19, 2024
c080cda
nit: added 404 response to empty user request
drisc Dec 19, 2024
5a6414a
refactor: added input validation for query param
drisc Dec 19, 2024
2e5126e
refactor: condensed set request query significantly
drisc Dec 19, 2024
7e22df5
fix: update test assertion for 404 response
drisc Dec 19, 2024
e38ab0f
Merge branch 'master' into setrequest-api
drisc Dec 19, 2024
d9eff95
refactor(api): optimise query based on feedback
drisc Dec 30, 2024
5a52888
refactor(test): adjust test for new query sorting
drisc Dec 30, 2024
2067907
Merge branch 'master' into setrequest-api
drisc Dec 30, 2024
351c0b4
Merge branch 'master' into setrequest-api
wescopeland Dec 30, 2024
8604e13
refactor(api): stricter query validation, remove map
drisc Dec 30, 2024
46d4cb9
Merge branch 'master' into setrequest-api
drisc Dec 31, 2024
a3036e3
Merge branch 'master' into setrequest-api
Jamiras Jan 1, 2025
436f70c
Merge branch 'master' into setrequest-api
drisc Jan 1, 2025
a678a8f
feat: added type flag to return all requests
drisc Jan 1, 2025
29dbab7
refactor(test): added test for new type arg
drisc Jan 1, 2025
c7ca65c
chore: lint
drisc Jan 1, 2025
45718e5
fix: applied correct input validation to type query
drisc Jan 1, 2025
9be5678
refactor: applied suggested code squash
drisc Jan 1, 2025
114a3a9
Merge branch 'master' into setrequest-api
drisc Jan 1, 2025
1177b27
nit: typo
drisc Jan 2, 2025
ea54b86
refactor: change magic string to enum
drisc Jan 2, 2025
32fd885
refactor(test): change magic string to enum, update function name
drisc Jan 2, 2025
041999e
Merge branch 'master' into setrequest-api
drisc Jan 2, 2025
06dbbff
Merge branch 'master' into setrequest-api
wescopeland Jan 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/Models/UserGameListEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
namespace App\Models;

use App\Support\Database\Eloquent\BaseModel;
use Database\Factories\UserGameListEntryFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;

class UserGameListEntry extends BaseModel
{
/** @use HasFactory<UserGameListEntryFactory> */
use HasFactory;

// TODO rename SetRequest to user_game_list_entry or integrate into player_games table
// TODO rename GameID to game_id
// TODO drop user_game_list_entry_username_game_id_type_unique
Expand All @@ -29,6 +34,11 @@ class UserGameListEntry extends BaseModel
'GameID' => 'integer',
];

protected static function newFactory(): UserGameListEntryFactory
{
return UserGameListEntryFactory::new();
}

// helpers

public static function getUserSetRequestsInformation(User $user): array
Expand Down
27 changes: 27 additions & 0 deletions database/factories/UserGameListEntryFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Database\Factories;

use App\Models\Game;
use App\Models\User;
use App\Models\UserGameListEntry;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
* @extends Factory<UserGameListEntry>
*/
class UserGameListEntryFactory extends Factory
{
protected $model = UserGameListEntry::class;

public function definition(): array
{
return [
'user_id' => User::factory(),
'type' => 'achievement_set_request',
'GameID' => Game::factory(),
];
}
}
76 changes: 76 additions & 0 deletions public/API/API_GetUserSetRequests.php
Comment thread
wescopeland marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

/*
* API_GetUserSetRequests - gets user's set request list
* u : username
* t : type. 0 = only active, 1 = all requests including completed ones (default: 0)
*
* array RequestedSets
* object [value]
* int GameID unique identifier of the game
* string Title set title
* int ConsoleID the id of the console that the set is for
* string ConsoleName the name of the console that the set is for
* string ImageIcon the url of the sets icon
* int TotalRequests maximum number of requests a user has
* int PointsForNext number of points remaining until maximum request increase
*/

use App\Models\User;
use App\Models\UserGameListEntry;
use App\Support\Rules\CtypeAlnum;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;

$input = Validator::validate(Arr::wrap(request()->query()), [
'u' => ['required', 'min:2', 'max:20', new CtypeAlnum()],
't' => ['nullable', 'in:0,1'],
]);

$user = User::firstWhere('User', request()->query('u'));
if (!$user) {
return response()->json([], 404);
}

$type = (int) request()->query('t');

if ($type === 1) {
$requestedSets = UserGameListEntry::select([
'GameData.id as GameID',
'GameData.Title',
'GameData.ImageIcon',
'GameData.ConsoleID',
'Console.Name as ConsoleName',
])
->join('GameData', 'SetRequest.GameID', '=', 'GameData.ID')
->join('Console', 'GameData.ConsoleID', '=', 'Console.ID')
->where('SetRequest.user_id', $user->id)
->where('type', 'achievement_set_request')
->orderBy('GameData.sort_title')
->get()
->toArray();
} else {
$requestedSets = UserGameListEntry::select([
'GameData.id as GameID',
'GameData.Title',
'GameData.ImageIcon',
'GameData.ConsoleID',
'Console.Name as ConsoleName',
])
->join('GameData', 'SetRequest.GameID', '=', 'GameData.ID')
->join('Console', 'GameData.ConsoleID', '=', 'Console.ID')
->where('SetRequest.user_id', $user->id)
->where('type', 'achievement_set_request')
->where('GameData.achievements_published', '=', '0')
->orderBy('GameData.sort_title')
->get()
->toArray();
}
Comment thread
Jamiras marked this conversation as resolved.

$userRequestInfo = UserGameListEntry::getUserSetRequestsInformation($user);

return response()->json([
'RequestedSets' => $requestedSets,
'TotalRequests' => $userRequestInfo['total'],
'PointsForNext' => $userRequestInfo['pointsForNext'],
]);
141 changes: 141 additions & 0 deletions tests/Feature/Api/V1/UserSetRequestsTest.php
Comment thread
wescopeland marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

declare(strict_types=1);

namespace Tests\Feature\Api\V1;

use App\Models\Game;
use App\Models\System;
use App\Models\User;
use App\Models\UserGameListEntry;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class UserSetRequestsTest extends TestCase
{
use RefreshDatabase;
use BootstrapsApiV1;

/**
* Test that the API returns an empty response for a non-existent user.
*/
public function testGetUserSetRequestsUnknownUser(): void
{
$this->get($this->apiUrl('GetUserSetRequests', ['u' => 'nonExistant']))
->assertStatus(404)
->assertJson([]);
}

/**
* Test that the API returns all set requests for an existing user.
*/
public function testGetAllUserSetRequests(): void
{
$system = System::factory()->create();
$game = Game::factory()->create([
'ConsoleID' => $system->ID,
'ImageIcon' => '/Images/001234.png',
'achievements_published' => 0,

]);
$game2 = Game::factory()->create([
'Title' => '~Hack~ Test Case',
'ConsoleID' => $system->ID,
'ImageIcon' => '/Images/001235.png',
'achievements_published' => 12,
]);

$user = User::factory()->create([
'RAPoints' => 1501, // enough points to set request total to 1
]);

// Create the first user game list entry for the set request
UserGameListEntry::factory()->create([
'user_id' => $user->ID,
'type' => 'achievement_set_request',
'GameID' => $game->ID,
]);
// Create the second user game list entry for the set request
UserGameListEntry::factory()->create([
'user_id' => $user->ID,
'type' => 'achievement_set_request',
'GameID' => $game2->ID,
]);

$this->get($this->apiUrl('GetUserSetRequests', ['u' => $user->User, 't' => 1]))
->assertSuccessful()
->assertJson([
'RequestedSets' => [
[
'GameID' => $game->ID,
'Title' => $game->Title,
'ConsoleID' => $game->ConsoleID,
'ConsoleName' => $system->Name,
'ImageIcon' => $game->ImageIcon,
],
[
'GameID' => $game2->ID,
'Title' => $game2->Title,
'ConsoleID' => $game2->ConsoleID,
'ConsoleName' => $system->Name,
'ImageIcon' => $game2->ImageIcon,
],
],
'TotalRequests' => 1,
'PointsForNext' => 999,
]);
}

/**
* Test that the API returns only set requests with no published achievements for an existing user.
*/
public function testGetUserSetRequests(): void
Comment thread
wescopeland marked this conversation as resolved.
Outdated
{
$system = System::factory()->create();
$game = Game::factory()->create([
'ConsoleID' => $system->ID,
'ImageIcon' => '/Images/001234.png',
'achievements_published' => 4,
]);
$game2 = Game::factory()->create([
'Title' => '~Hack~ Test Case',
'ConsoleID' => $system->ID,
'ImageIcon' => '/Images/001235.png',
'achievements_published' => 0,
]);

$user = User::factory()->create([
'RAPoints' => 1501, // enough points to set request total to 1
]);

// Create the first user game list entry for the set request
UserGameListEntry::factory()->create([
'user_id' => $user->ID,
'type' => 'achievement_set_request',
'GameID' => $game->ID,
]);
// Create the second user game list entry for the set request
UserGameListEntry::factory()->create([
'user_id' => $user->ID,
'type' => 'achievement_set_request',
'GameID' => $game2->ID,
]);

// Note that only the second game is present in the assert. The code will find the first game as well but only return the game with no published achievements.
Comment thread
drisc marked this conversation as resolved.
Outdated
$this->get($this->apiUrl('GetUserSetRequests', ['u' => $user->User, 't' => 0]))
->assertSuccessful()
->assertJson([
'RequestedSets' => [
[
'GameID' => $game2->ID,
'Title' => $game2->Title,
'ConsoleID' => $game2->ConsoleID,
'ConsoleName' => $system->Name,
'ImageIcon' => $game2->ImageIcon,
],
],
'TotalRequests' => 1,
'PointsForNext' => 999,
]);
}
}