mirror of
https://github.com/notf0und/SGS
synced 2026-07-18 02:31:08 +00:00
### Changes
Fixes #3: Error on DBI when files or folders contains special characters. ### Additional changes * Added unit and feature tests * Fix file size display * Run tests on CI/CD * Publish docker image on release
This commit is contained in:
parent
15b9c17b99
commit
0993bfa666
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
beforeEach(function () {
|
||||
Storage::fake();
|
||||
});
|
||||
|
||||
test('returns an HTML directory listing for the games root', function () {
|
||||
Storage::put('games/GameA/game.nsp', 'data');
|
||||
|
||||
$response = $this->get('/api/dbi/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
$response->assertSee('Index of /games/');
|
||||
});
|
||||
|
||||
test('lists directories that contain installable files', function () {
|
||||
Storage::put('games/GameA/game.nsp', 'data');
|
||||
Storage::put('games/GameB/game.xci', 'data');
|
||||
|
||||
$response = $this->get('/api/dbi/');
|
||||
|
||||
$response->assertSee('GameA');
|
||||
$response->assertSee('GameB');
|
||||
});
|
||||
|
||||
test('excludes directories with no installable files', function () {
|
||||
Storage::put('games/HasGames/game.nsp', 'data');
|
||||
Storage::put('games/NoGames/readme.txt', 'data');
|
||||
|
||||
$response = $this->get('/api/dbi/');
|
||||
|
||||
$response->assertSee('HasGames');
|
||||
$response->assertDontSee('NoGames');
|
||||
});
|
||||
|
||||
test('lists only files with supported extensions', function () {
|
||||
Storage::put('games/game.nsp', 'data');
|
||||
Storage::put('games/game.nsz', 'data');
|
||||
Storage::put('games/game.xci', 'data');
|
||||
Storage::put('games/game.xcz', 'data');
|
||||
Storage::put('games/readme.txt', 'data');
|
||||
|
||||
$response = $this->get('/api/dbi/');
|
||||
|
||||
$response->assertSee('game.nsp');
|
||||
$response->assertSee('game.nsz');
|
||||
$response->assertSee('game.xci');
|
||||
$response->assertSee('game.xcz');
|
||||
$response->assertDontSee('readme.txt');
|
||||
});
|
||||
|
||||
test('shows a parent directory link when inside a subdirectory', function () {
|
||||
Storage::put('games/GameA/game.nsp', 'data');
|
||||
|
||||
$response = $this->get('/api/dbi/GameA/');
|
||||
|
||||
$response->assertSee('Parent Directory');
|
||||
});
|
||||
|
||||
test('does not show a parent directory link at the root', function () {
|
||||
Storage::put('games/GameA/game.nsp', 'data');
|
||||
|
||||
$response = $this->get('/api/dbi/');
|
||||
|
||||
$response->assertDontSee('Parent Directory');
|
||||
});
|
||||
|
||||
test('directory links use rawurlencode so special characters are encoded', function () {
|
||||
Storage::put('games/Game Title/game.nsp', 'data');
|
||||
Storage::put('games/Special & Chars/game.nsp', 'data');
|
||||
|
||||
$response = $this->get('/api/dbi/');
|
||||
|
||||
$response->assertSee('Game%20Title');
|
||||
$response->assertSee('Special%20%26%20Chars');
|
||||
});
|
||||
|
||||
test('folder names with a plus sign are encoded as %2B in links, not as a space', function () {
|
||||
Storage::put('games/Game+Title/game.nsp', 'data');
|
||||
|
||||
$response = $this->get('/api/dbi/');
|
||||
|
||||
// The href must use %2B, not a literal + (which urlencode would produce)
|
||||
$response->assertSee('Game%2BTitle');
|
||||
// The display name is rendered as-is; only the href encoding matters
|
||||
$response->assertDontSee('Game%20Title');
|
||||
$response->assertDontSee('Game Title');
|
||||
});
|
||||
|
||||
test('navigating into a folder with a plus sign returns a listing', function () {
|
||||
Storage::put('games/Game+Title/game.nsp', 'data');
|
||||
|
||||
// The client follows the %2B-encoded link
|
||||
$response = $this->get('/api/dbi/Game%2BTitle/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertSee('game.nsp');
|
||||
});
|
||||
|
||||
test('DBI user-agent receives relative links', function () {
|
||||
Storage::put('games/GameA/game.nsp', 'data');
|
||||
|
||||
$response = $this->get('/api/dbi/', ['HTTP_USER_AGENT' => 'DBI/1.0']);
|
||||
|
||||
// DBI clients need a relative href like "GameA/" not an absolute path
|
||||
$response->assertSee('href="' . rawurlencode('GameA') . '/"', false);
|
||||
});
|
||||
|
||||
test('browser user-agent receives absolute links', function () {
|
||||
Storage::put('games/GameA/game.nsp', 'data');
|
||||
|
||||
$response = $this->get('/api/dbi/');
|
||||
|
||||
$response->assertSee('href="/api/dbi/' . rawurlencode('GameA') . '"', false);
|
||||
});
|
||||
|
||||
test('displays the current subdirectory path in the page title', function () {
|
||||
Storage::put('games/GameA/game.nsp', 'data');
|
||||
|
||||
$response = $this->get('/api/dbi/GameA/');
|
||||
|
||||
$response->assertSee('Index of /games/GameA/');
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
beforeEach(function () {
|
||||
Storage::fake();
|
||||
});
|
||||
|
||||
test('serves a file as an octet-stream download', function () {
|
||||
Storage::put('games/game.nsp', 'binary-content');
|
||||
|
||||
$response = $this->get('/api/dbi/game.nsp');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertHeader('Content-Type', 'application/octet-stream');
|
||||
});
|
||||
|
||||
test('returns 404 when the requested file does not exist', function () {
|
||||
$response = $this->get('/api/dbi/missing.nsp');
|
||||
|
||||
$response->assertStatus(404);
|
||||
});
|
||||
|
||||
test('serves a file inside a subdirectory', function () {
|
||||
Storage::put('games/GameA/game.nsp', 'binary-content');
|
||||
|
||||
$response = $this->get('/api/dbi/GameA/game.nsp');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertHeader('Content-Type', 'application/octet-stream');
|
||||
});
|
||||
|
||||
test('serves a file whose folder name contains a plus sign', function () {
|
||||
Storage::put('games/Game+Title/game.nsp', 'binary-content');
|
||||
|
||||
// %2B in the URL must decode to a literal + (not a space)
|
||||
$response = $this->get('/api/dbi/Game%2BTitle/game.nsp');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertHeader('Content-Type', 'application/octet-stream');
|
||||
});
|
||||
|
||||
test('returns 404 when plus sign is misread as a space', function () {
|
||||
Storage::put('games/Game+Title/game.nsp', 'binary-content');
|
||||
|
||||
// A literal + in the URL path should NOT match a folder named "Game+Title"
|
||||
// because + must be decoded as %2B, not as a space
|
||||
$response = $this->get('/api/dbi/Game+Title/game.nsp');
|
||||
|
||||
// "Game+Title" decoded with rawurldecode stays "Game+Title",
|
||||
// which correctly finds the file.
|
||||
$response->assertStatus(200);
|
||||
});
|
||||
|
||||
test('serves a file with a plus sign in the filename', function () {
|
||||
Storage::put('games/Game+Edition.nsp', 'binary-content');
|
||||
|
||||
$response = $this->get('/api/dbi/Game%2BEdition.nsp');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertHeader('Content-Type', 'application/octet-stream');
|
||||
});
|
||||
|
||||
test('response includes Accept-Ranges header for range request support', function () {
|
||||
Storage::put('games/game.nsp', 'binary-content');
|
||||
|
||||
$response = $this->get('/api/dbi/game.nsp');
|
||||
|
||||
$response->assertHeader('Accept-Ranges', 'bytes');
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
test('the application returns a successful response', function () {
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
beforeEach(function () {
|
||||
Storage::fake();
|
||||
});
|
||||
|
||||
test('a DBI user-agent on the web root gets a DBI directory listing', function () {
|
||||
Storage::put('games/game.nsp', 'data');
|
||||
|
||||
$response = $this->get('/', ['HTTP_USER_AGENT' => 'DBI/1.0']);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
$response->assertSee('Index of /games/');
|
||||
});
|
||||
|
||||
test('a non-DBI user-agent on the web root is passed through', function () {
|
||||
$response = $this->get('/', ['HTTP_USER_AGENT' => 'Mozilla/5.0']);
|
||||
|
||||
// The welcome view or the next middleware handles the request (not DBI)
|
||||
$response->assertDontSee('Index of /games/');
|
||||
});
|
||||
|
||||
test('a DBI user-agent on an arbitrary web path gets a DBI directory listing', function () {
|
||||
Storage::put('games/GameA/game.nsp', 'data');
|
||||
|
||||
$response = $this->get('/anything', ['HTTP_USER_AGENT' => 'DBI/2.5']);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertSee('Index of /games/');
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
test('a request with a theme header on the web root redirects to the tinfoil route', function () {
|
||||
$response = $this->get('/', ['HTTP_THEME' => 'default']);
|
||||
|
||||
$response->assertRedirect(route('tinfoil'));
|
||||
});
|
||||
|
||||
test('a request without a theme header on the web root is not redirected', function () {
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
beforeEach(function () {
|
||||
Storage::fake();
|
||||
});
|
||||
|
||||
test('returns a JSON response with files and success keys', function () {
|
||||
Storage::put('game.nsp', 'data');
|
||||
|
||||
$response = $this->get('/api/tinfoil');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertJsonStructure(['files', 'success']);
|
||||
});
|
||||
|
||||
test('success value equals the application name', function () {
|
||||
$response = $this->get('/api/tinfoil');
|
||||
|
||||
$response->assertJsonPath('success', config('app.name'));
|
||||
});
|
||||
|
||||
test('returns an empty files array when there are no installable files', function () {
|
||||
$response = $this->get('/api/tinfoil');
|
||||
|
||||
$response->assertJsonPath('files', []);
|
||||
});
|
||||
|
||||
test('each file entry contains a url and a size', function () {
|
||||
Storage::put('game.nsp', 'binary-content');
|
||||
|
||||
$response = $this->get('/api/tinfoil');
|
||||
|
||||
$response->assertJsonStructure(['files' => [['url', 'size']]]);
|
||||
});
|
||||
|
||||
test('only includes files with supported Nintendo Switch extensions', function () {
|
||||
Storage::put('game.nsp', 'data');
|
||||
Storage::put('game.nsz', 'data');
|
||||
Storage::put('game.xci', 'data');
|
||||
Storage::put('game.xcz', 'data');
|
||||
Storage::put('readme.txt', 'data');
|
||||
Storage::put('cover.jpg', 'data');
|
||||
|
||||
$response = $this->get('/api/tinfoil');
|
||||
|
||||
$files = collect($response->json('files'));
|
||||
expect($files)->toHaveCount(4);
|
||||
expect($files->pluck('url')->every(fn ($url) => !str_ends_with($url, '.txt') && !str_ends_with($url, '.jpg')))->toBeTrue();
|
||||
});
|
||||
|
||||
test('file url is an absolute URL', function () {
|
||||
Storage::put('game.nsp', 'data');
|
||||
|
||||
$response = $this->get('/api/tinfoil');
|
||||
|
||||
$url = $response->json('files.0.url');
|
||||
expect($url)->toStartWith('http');
|
||||
});
|
||||
|
||||
test('file size matches the stored file size', function () {
|
||||
$content = str_repeat('x', 1024);
|
||||
Storage::put('game.nsp', $content);
|
||||
|
||||
$response = $this->get('/api/tinfoil');
|
||||
|
||||
$size = $response->json('files.0.size');
|
||||
expect($size)->toBe(1024);
|
||||
});
|
||||
Reference in New Issue
Block a user