mirror of
https://github.com/notf0und/SGS
synced 2026-07-17 18:21:06 +00:00
Merge pull request #4 from notf0und/fix-error-on-dbi-files-and-folders-with-special-characters
Fixes #3: Error on DBI when files or folders contains special characters.
This commit is contained in:
committed by
GitHub
commit
fbf121553f
@@ -0,0 +1,100 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run test suite
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.4'
|
||||
extensions: mbstring
|
||||
coverage: none
|
||||
|
||||
- name: Install Composer dependencies
|
||||
working-directory: www
|
||||
run: composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Set up environment
|
||||
working-directory: www
|
||||
run: |
|
||||
cp .env.example .env
|
||||
php artisan key:generate
|
||||
|
||||
- name: Build frontend assets
|
||||
working-directory: www
|
||||
run: npm ci && npm run build
|
||||
|
||||
- name: Run tests
|
||||
working-directory: www
|
||||
run: php artisan test
|
||||
|
||||
lint:
|
||||
name: Check code style (Pint)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.4'
|
||||
extensions: mbstring
|
||||
coverage: none
|
||||
|
||||
- name: Install Composer dependencies
|
||||
working-directory: www
|
||||
run: composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Run Pint
|
||||
working-directory: www
|
||||
run: ./vendor/bin/pint --test
|
||||
|
||||
docker:
|
||||
name: Build and push Docker image
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, lint]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU (multi-platform support)
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: notf0und/sgs
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
@@ -0,0 +1,62 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ['**']
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run test suite
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.4'
|
||||
extensions: mbstring
|
||||
coverage: none
|
||||
|
||||
- name: Install Composer dependencies
|
||||
working-directory: www
|
||||
run: composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Set up environment
|
||||
working-directory: www
|
||||
run: |
|
||||
cp .env.example .env
|
||||
php artisan key:generate
|
||||
|
||||
- name: Build frontend assets
|
||||
working-directory: www
|
||||
run: npm ci && npm run build
|
||||
|
||||
- name: Run tests
|
||||
working-directory: www
|
||||
run: php artisan test
|
||||
|
||||
lint:
|
||||
name: Check code style (Pint)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.4'
|
||||
extensions: mbstring
|
||||
coverage: none
|
||||
|
||||
- name: Install Composer dependencies
|
||||
working-directory: www
|
||||
run: composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Run Pint
|
||||
working-directory: www
|
||||
run: ./vendor/bin/pint --test
|
||||
@@ -10,3 +10,4 @@
|
||||
/games/*
|
||||
!/games/.gitkeep
|
||||
Thumbs.db
|
||||
docker-compose.override.yml
|
||||
|
||||
@@ -18,7 +18,7 @@ enum NintendoFileExtension: string
|
||||
*/
|
||||
public static function values(): array
|
||||
{
|
||||
return array_map(fn($case) => $case->value, self::cases());
|
||||
return array_map(fn ($case) => $case->value, self::cases());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Helpers;
|
||||
class FileSize
|
||||
{
|
||||
private const UNITS = ['', 'K', 'M', 'G', 'T'];
|
||||
|
||||
private const THRESHOLD = 1024;
|
||||
|
||||
/**
|
||||
@@ -18,9 +19,10 @@ class FileSize
|
||||
|
||||
$unitIndex = (int) floor(log($bytes, self::THRESHOLD));
|
||||
$unitIndex = min($unitIndex, count(self::UNITS) - 1);
|
||||
|
||||
|
||||
$size = $bytes / pow(self::THRESHOLD, $unitIndex);
|
||||
|
||||
return number_format($size, 2) . self::UNITS[$unitIndex];
|
||||
$decimals = $unitIndex === 0 ? 0 : 2;
|
||||
|
||||
return number_format($size, $decimals, '.', '').self::UNITS[$unitIndex];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ class IndexController extends Controller
|
||||
private function getRequestPath(Request $request): ?string
|
||||
{
|
||||
$path = $request->route('path');
|
||||
return $path ? urldecode($path) : null;
|
||||
|
||||
return $path ? rawurldecode($path) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,9 +52,9 @@ class IndexController extends Controller
|
||||
*/
|
||||
private function serveFile(string $path): Response|BinaryFileResponse
|
||||
{
|
||||
$storagePath = self::GAMES_DIRECTORY . '/' . $path;
|
||||
$storagePath = self::GAMES_DIRECTORY.'/'.$path;
|
||||
|
||||
if (!Storage::exists($storagePath)) {
|
||||
if (! Storage::exists($storagePath)) {
|
||||
return response('File not found', 404);
|
||||
}
|
||||
|
||||
@@ -68,19 +69,19 @@ class IndexController extends Controller
|
||||
*/
|
||||
private function serveDirectoryListing(?string $path, Request $request): Response
|
||||
{
|
||||
$storagePath = $path
|
||||
? self::GAMES_DIRECTORY . '/' . $path
|
||||
$storagePath = $path
|
||||
? self::GAMES_DIRECTORY.'/'.$path
|
||||
: self::GAMES_DIRECTORY;
|
||||
|
||||
$directories = $this->getDirectories($storagePath);
|
||||
$files = $this->getFiles($storagePath);
|
||||
|
||||
|
||||
// Determine if this is a DBI client or regular browser
|
||||
$isDBI = str_starts_with($request->userAgent() ?? '', 'DBI/');
|
||||
|
||||
|
||||
// Determine base URL for links based on request path
|
||||
$baseUrl = str_starts_with($request->path(), 'api/dbi') ? '/api/dbi/' : '/';
|
||||
|
||||
|
||||
$html = $this->buildDirectoryListingHtml($directories, $files, $path ?? '', $baseUrl, $isDBI);
|
||||
|
||||
return response($html)->header('Content-Type', 'text/html');
|
||||
@@ -131,8 +132,8 @@ class IndexController extends Controller
|
||||
*/
|
||||
private function buildDirectoryListingHtml(Collection $directories, Collection $files, string $currentPath, string $baseUrl, bool $isDBI): string
|
||||
{
|
||||
$displayPath = $currentPath
|
||||
? '/games/' . urldecode($currentPath) . '/'
|
||||
$displayPath = $currentPath
|
||||
? '/games/'.rawurldecode($currentPath).'/'
|
||||
: '/games/';
|
||||
|
||||
return view('dbi.directory-listing', [
|
||||
@@ -151,7 +152,7 @@ class IndexController extends Controller
|
||||
*/
|
||||
private function getParentDirectoryUrl(string $currentPath, string $baseUrl, bool $isDBI): ?string
|
||||
{
|
||||
if (!$currentPath) {
|
||||
if (! $currentPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -167,8 +168,8 @@ class IndexController extends Controller
|
||||
return $baseUrl;
|
||||
}
|
||||
|
||||
return rtrim($baseUrl, '/') . '/' . collect(explode('/', $parentPath))
|
||||
return rtrim($baseUrl, '/').'/'.collect(explode('/', $parentPath))
|
||||
->map(fn ($segment) => rawurlencode($segment))
|
||||
->implode('/');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class IndexController extends Controller
|
||||
{
|
||||
return response()->json([
|
||||
'files' => $this->getInstallableFiles()->values(),
|
||||
'success' => config('app.name')
|
||||
'success' => config('app.name'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class IndexController extends Controller
|
||||
->filter(fn ($path) => NintendoFileExtension::isSupported($path))
|
||||
->map(fn ($path) => [
|
||||
'url' => url($path),
|
||||
'size' => Storage::size($path)
|
||||
'size' => Storage::size($path),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,17 @@ use Closure;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class EnsureIsTinfoilClient
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure(Request): (Response|RedirectResponse) $next
|
||||
* @return Response|RedirectResponse
|
||||
* @param Closure(Request): (Response|RedirectResponse) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response|RedirectResponse
|
||||
{
|
||||
if (!!$request->header('theme')) {
|
||||
if ((bool) $request->header('theme')) {
|
||||
return redirect()->route('tinfoil');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Providers\AppServiceProvider;
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
AppServiceProvider::class,
|
||||
];
|
||||
|
||||
+2
-2
@@ -9,14 +9,14 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"php": "^8.4",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.24",
|
||||
"laravel/pint": "^1.29",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
|
||||
Generated
+14
-13
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "cf274898566d29ebdc912d11f0d0a6e3",
|
||||
"content-hash": "f10e5685efd94a89d824ca6ed03c8b85",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -6525,16 +6525,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/pint",
|
||||
"version": "v1.26.0",
|
||||
"version": "v1.29.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/pint.git",
|
||||
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f"
|
||||
"reference": "bdec963f53172c5e36330f3a400604c69bf02d39"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f",
|
||||
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f",
|
||||
"url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39",
|
||||
"reference": "bdec963f53172c5e36330f3a400604c69bf02d39",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6545,13 +6545,14 @@
|
||||
"php": "^8.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.90.0",
|
||||
"illuminate/view": "^12.40.1",
|
||||
"larastan/larastan": "^3.8.0",
|
||||
"laravel-zero/framework": "^12.0.4",
|
||||
"friendsofphp/php-cs-fixer": "^3.94.2",
|
||||
"illuminate/view": "^12.54.1",
|
||||
"larastan/larastan": "^3.9.3",
|
||||
"laravel-zero/framework": "^12.0.5",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"nunomaduro/termwind": "^2.3.3",
|
||||
"pestphp/pest": "^3.8.4"
|
||||
"nunomaduro/termwind": "^2.4.0",
|
||||
"pestphp/pest": "^3.8.6",
|
||||
"shipfastlabs/agent-detector": "^1.1.0"
|
||||
},
|
||||
"bin": [
|
||||
"builds/pint"
|
||||
@@ -6588,7 +6589,7 @@
|
||||
"issues": "https://github.com/laravel/pint/issues",
|
||||
"source": "https://github.com/laravel/pint"
|
||||
},
|
||||
"time": "2025-11-25T21:15:52+00:00"
|
||||
"time": "2026-03-12T15:51:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/sail",
|
||||
@@ -9339,7 +9340,7 @@
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": "^8.2"
|
||||
"php": "^8.4"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
@@ -62,7 +64,7 @@ return [
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
'model' => env('AUTH_MODEL', User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Pdo\Mysql;
|
||||
|
||||
return [
|
||||
|
||||
@@ -59,7 +60,7 @@ return [
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
@@ -79,7 +80,7 @@ return [
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
|
||||
+4
-2
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\DBI\IndexController as DBIIndexController;
|
||||
use App\Http\Controllers\Tinfoil\IndexController as TinfoilIndexController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/*
|
||||
@@ -7,9 +9,9 @@ use Illuminate\Support\Facades\Route;
|
||||
| API Routes
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::get('tinfoil', \App\Http\Controllers\Tinfoil\IndexController::class)
|
||||
Route::get('tinfoil', TinfoilIndexController::class)
|
||||
->name('tinfoil');
|
||||
|
||||
Route::get('dbi/{path?}', \App\Http\Controllers\DBI\IndexController::class)
|
||||
Route::get('dbi/{path?}', DBIIndexController::class)
|
||||
->where('path', '.*')
|
||||
->name('dbi');
|
||||
|
||||
+9
-9
@@ -6,37 +6,37 @@ use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::view('/', 'welcome')->middleware([
|
||||
EnsureIsDbiClient::class,
|
||||
EnsureIsTinfoilClient::class
|
||||
EnsureIsTinfoilClient::class,
|
||||
]);
|
||||
|
||||
Route::get('/docs/{page}', function ($page) {
|
||||
$allowedPages = ['TINFOIL', 'DBI'];
|
||||
if (!in_array($page, $allowedPages)) {
|
||||
if (! in_array($page, $allowedPages)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
|
||||
$content = file_get_contents(base_path("docs/{$page}.md"));
|
||||
|
||||
|
||||
// Convert relative image paths to absolute paths for web viewing
|
||||
$content = str_replace(
|
||||
'](images/',
|
||||
'](/docs-images/',
|
||||
$content
|
||||
);
|
||||
|
||||
|
||||
return view('docs', ['content' => $content, 'title' => $page]);
|
||||
})->name('docs');
|
||||
|
||||
Route::get('/docs-images/{path}', function ($path) {
|
||||
$imagePath = base_path("docs/images/{$path}");
|
||||
|
||||
if (!file_exists($imagePath)) {
|
||||
|
||||
if (! file_exists($imagePath)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
|
||||
return response()->file($imagePath);
|
||||
})->where('path', '.*')->name('docs.images');
|
||||
|
||||
Route::get('/{path}', fn() => response('Not Found', 404))
|
||||
Route::get('/{path}', fn () => response('Not Found', 404))
|
||||
->where('path', '(?!api|docs-images).*')
|
||||
->middleware(EnsureIsDbiClient::class);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Test Case
|
||||
@@ -11,7 +13,7 @@
|
||||
|
|
||||
*/
|
||||
|
||||
pest()->extend(Tests\TestCase::class)
|
||||
pest()->extend(TestCase::class)
|
||||
// ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
|
||||
->in('Feature');
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
test('that true is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use App\Helpers\FileSize;
|
||||
|
||||
test('formats zero bytes as "0"', function () {
|
||||
expect(FileSize::format(0))->toBe('0');
|
||||
});
|
||||
|
||||
test('formats byte values without decimal places', function () {
|
||||
expect(FileSize::format(1))->toBe('1');
|
||||
expect(FileSize::format(512))->toBe('512');
|
||||
expect(FileSize::format(1001))->toBe('1001');
|
||||
expect(FileSize::format(1023))->toBe('1023');
|
||||
});
|
||||
|
||||
test('formats kilobytes', function () {
|
||||
expect(FileSize::format(1024))->toBe('1.00K');
|
||||
expect(FileSize::format(1536))->toBe('1.50K');
|
||||
});
|
||||
|
||||
test('formats megabytes', function () {
|
||||
expect(FileSize::format(1024 * 1024))->toBe('1.00M');
|
||||
expect(FileSize::format((int) (1.5 * 1024 * 1024)))->toBe('1.50M');
|
||||
});
|
||||
|
||||
test('formats gigabytes', function () {
|
||||
expect(FileSize::format(1024 * 1024 * 1024))->toBe('1.00G');
|
||||
});
|
||||
|
||||
test('formats terabytes', function () {
|
||||
expect(FileSize::format(1024 * 1024 * 1024 * 1024))->toBe('1.00T');
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\NintendoFileExtension;
|
||||
|
||||
test('values() returns all four supported extensions', function () {
|
||||
expect(NintendoFileExtension::values())->toBe(['nsp', 'nsz', 'xci', 'xcz']);
|
||||
});
|
||||
|
||||
test('isSupported() returns true for each supported extension', function (string $filename) {
|
||||
expect(NintendoFileExtension::isSupported($filename))->toBeTrue();
|
||||
})->with(['game.nsp', 'game.nsz', 'game.xci', 'game.xcz']);
|
||||
|
||||
test('isSupported() is case-insensitive', function (string $filename) {
|
||||
expect(NintendoFileExtension::isSupported($filename))->toBeTrue();
|
||||
})->with(['game.NSP', 'game.NSZ', 'game.XCI', 'game.XCZ', 'game.Nsp', 'game.Xci']);
|
||||
|
||||
test('isSupported() returns false for unsupported extensions', function (string $filename) {
|
||||
expect(NintendoFileExtension::isSupported($filename))->toBeFalse();
|
||||
})->with(['game.txt', 'game.exe', 'game.zip', 'game.iso', 'game.php', 'game.rom']);
|
||||
|
||||
test('isSupported() returns false for files without an extension', function () {
|
||||
expect(NintendoFileExtension::isSupported('gamefile'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('isSupported() works with paths containing directory separators', function () {
|
||||
expect(NintendoFileExtension::isSupported('folder/game.nsp'))->toBeTrue();
|
||||
expect(NintendoFileExtension::isSupported('folder/sub/game.xci'))->toBeTrue();
|
||||
expect(NintendoFileExtension::isSupported('folder/game.txt'))->toBeFalse();
|
||||
});
|
||||
Reference in New Issue
Block a user