Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion app/Http/Controllers/PluginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public function __construct(private RuneliteApiService $runeliteApi) {}
public function index(Request $request): Response
{
$params = $request->only(['range']);
$plugins = $this->runeliteApi->getPlugins($params);
$plugins = $this->runeliteApi->getClientPlugins($params);

SEOTools::setTitle('RuneLite Plugin Stats — Browse All Plugins');
SEOTools::setDescription('Browse install counts, all-time highs for every RuneLite plugins.');
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Middleware/HandleInertiaRequests.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function share(Request $request): array
'name' => config('app.name'),
'apiUrl' => rtrim(config('services.runelite_api.client_url'), '/'),
'appUrl' => config('app.url'),
'plugins' => $this->runeliteApi->getPlugins([]),
'plugins' => $this->runeliteApi->getClientPlugins([]),
];
}
}
37 changes: 37 additions & 0 deletions app/Services/RuneliteApiService.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@ class RuneliteApiService
/** @var array{0: array<string, array<mixed>>, 1: array<string, array<mixed>>}|null */
private ?array $developerLookups = null;

/**
* Fields the browser actually reads off the shared plugin list: the homepage
* table and the header search. This list is embedded in every Inertia
* response, so anything the client never touches is dead weight on the wire.
*
* @var list<string>
*/
private const CLIENT_PLUGIN_FIELDS = [
'id',
'name',
'display',
'author',
'description',
'tags',
'warning',
'updated_on',
'all_time_high',
'current_installs',
];

public function __construct()
{
$this->baseUrl = rtrim(config('services.runelite_api.url'), '/');
Expand All @@ -25,6 +45,23 @@ public function getPlugins(array $params = []): array
});
}

/**
* The plugin list as it is handed to the client, stripped to the fields the
* frontend reads. `git_repo`, `support` and `created_on` are only ever read
* off the single-plugin and ranking-entry props, never off this list.
*
* @return array<mixed>
*/
public function getClientPlugins(array $params = []): array
{
$keep = array_flip(self::CLIENT_PLUGIN_FIELDS);

return array_map(
fn (array $plugin): array => array_intersect_key($plugin, $keep),
$this->getPlugins($params),
);
}

/** @return array<mixed>|null */
public function getPlugin(string $name, array $params = []): ?array
{
Expand Down
75 changes: 75 additions & 0 deletions tests/Feature/ClientPluginPayloadTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace Tests\Feature;

use Illuminate\Support\Facades\Http;
use Tests\TestCase;

/**
* The plugin list is a shared Inertia prop, so it is embedded in the HTML of
* every page. These tests pin the shape that reaches the browser.
*/
class ClientPluginPayloadTest extends TestCase
{
public function test_it_drops_fields_the_client_never_reads(): void
{
$this->fakeRuneliteApi();

$plugins = $this->sharedPlugins();

$this->assertCount(1, $plugins);

foreach (['git_repo', 'support', 'created_on'] as $dropped) {
$this->assertArrayNotHasKey($dropped, $plugins[0]);
}
}

public function test_it_keeps_every_field_the_table_and_search_read(): void
{
$this->fakeRuneliteApi();

$plugins = $this->sharedPlugins();

foreach ([
'id', 'name', 'display', 'author', 'description', 'tags',
'warning', 'updated_on', 'all_time_high', 'current_installs',
] as $kept) {
$this->assertArrayHasKey($kept, $plugins[0]);
}
}

/** @return array<int, array<string, mixed>> */
private function sharedPlugins(): array
{
$response = $this->get(route('home'));

$response->assertOk();

return $response->viewData('page')['props']['plugins'];
}

private function fakeRuneliteApi(): void
{
$plugin = [
'id' => 1,
'name' => 'example-plugin',
'git_repo' => 'https://github.com/example/example-plugin.git',
'display' => 'Example Plugin',
'author' => 'Example Author',
'support' => 'https://discord.gg/example',
'description' => 'An example plugin.',
'tags' => 'example,test',
'warning' => '',
'created_on' => '2024-01-01T00:00:00.000Z',
'updated_on' => '2024-06-01T00:00:00.000Z',
'all_time_high' => 200,
'current_installs' => 100,
];

Http::fake([
'*/plugins' => Http::response(['success' => true, 'data' => [$plugin]]),
'*/developers' => Http::response(['success' => true, 'data' => ['developers' => []]]),
'*' => Http::response(['success' => true, 'data' => []]),
]);
}
}