mirror of
https://github.com/linuxserver/Heimdall.git
synced 2026-08-07 07:16:13 +00:00
Add upgrade-regression test coverage
Add tests guarding the surfaces most likely to break on a future
Laravel/PHP upgrade:
- Helper globals: format_bytes, parse_size, className, get_brightness,
title_color (tests/Unit/helpers)
- CSRF exception config actually reaches the framework
(PreventRequestForgery neverVerify list) and the excepted routes resolve
- Core GET routes boot and render on the current framework
- Filesystem disks resolve and the local disk root stays pinned to
storage_path('app') (guards the Laravel 12 default-root change)
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Controllers\ItemController;
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use ReflectionClass;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Guards the CSRF configuration wired up in bootstrap/app.php.
|
||||
*
|
||||
* Laravel's request-forgery middleware self-disables while running unit tests,
|
||||
* so we cannot observe CSRF at request time. Instead we assert the *configuration*
|
||||
* directly: the three excluded URIs really are registered on the framework's
|
||||
* PreventRequestForgery middleware, and the routes those exceptions cover still
|
||||
* resolve to the expected controller actions. Either check would fail if a
|
||||
* framework upgrade renamed / deprecated the exception API (validateCsrfTokens()
|
||||
* now proxies preventRequestForgery()) or changed how the except list is stored,
|
||||
* or if the AJAX routes were dropped.
|
||||
*/
|
||||
class CsrfExceptionsTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function csrfExceptUris(): array
|
||||
{
|
||||
// The except list is stored in the protected static $neverVerify
|
||||
// property that Middleware::validateCsrfTokens(except: [...]) feeds.
|
||||
$reflection = new ReflectionClass(PreventRequestForgery::class);
|
||||
$property = $reflection->getProperty('neverVerify');
|
||||
$property->setAccessible(true);
|
||||
|
||||
return (array) $property->getValue();
|
||||
}
|
||||
|
||||
public function test_ajax_routes_are_registered_as_csrf_exceptions(): void
|
||||
{
|
||||
$except = $this->csrfExceptUris();
|
||||
|
||||
$this->assertContains('order', $except);
|
||||
$this->assertContains('appload', $except);
|
||||
$this->assertContains('test_config', $except);
|
||||
}
|
||||
|
||||
public function test_csrf_excepted_routes_resolve_to_the_expected_actions(): void
|
||||
{
|
||||
$expected = [
|
||||
'items.order' => ['order', 'setOrder'],
|
||||
'appload' => ['appload', 'appload'],
|
||||
'test_config' => ['test_config', 'testConfig'],
|
||||
];
|
||||
|
||||
foreach ($expected as $name => [$uri, $method]) {
|
||||
$route = Route::getRoutes()->getByName($name);
|
||||
|
||||
$this->assertNotNull($route, "Route [{$name}] is not registered.");
|
||||
$this->assertSame($uri, $route->uri());
|
||||
$this->assertContains('POST', $route->methods());
|
||||
$this->assertSame(ItemController::class . '@' . $method, $route->getActionName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Broad "the app still boots and renders on this Laravel version" safety net.
|
||||
*
|
||||
* Beyond the focused per-feature tests, this walks the main GET surface of the
|
||||
* app in one place and asserts every route returns its expected status with no
|
||||
* exception. A framework/PHP upgrade that broke view rendering, routing, the
|
||||
* auth scaffolding or the auth middleware would surface here as a 500 / wrong
|
||||
* status even if a more specific test was missing.
|
||||
*/
|
||||
class RoutesRenderTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_core_get_routes_boot_without_error(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$routes = [
|
||||
'/' => 200,
|
||||
'/login' => 200,
|
||||
'/userselect' => 200,
|
||||
'/settings' => 200,
|
||||
'/items' => 200,
|
||||
'/items/create' => 200,
|
||||
'/tags' => 200,
|
||||
'/health' => 200,
|
||||
'/up' => 200,
|
||||
];
|
||||
|
||||
foreach ($routes as $uri => $expectedStatus) {
|
||||
$response = $this->get($uri);
|
||||
|
||||
$this->assertSame(
|
||||
$expectedStatus,
|
||||
$response->getStatusCode(),
|
||||
"GET {$uri} returned {$response->getStatusCode()}, expected {$expectedStatus}."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_home_redirects_guests_to_login(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
// /home is behind the auth middleware; a guest must be redirected to
|
||||
// the login route (redirectGuestsTo in bootstrap/app.php).
|
||||
$response = $this->get('/home');
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
}
|
||||
|
||||
public function test_search_redirects_to_the_provider(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->get('/search?provider=google&q=heimdall');
|
||||
|
||||
$response->assertStatus(302);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Guards the filesystem configuration the icon / avatar upload paths depend on.
|
||||
*
|
||||
* config/filesystems.php explicitly pins the local disk root to
|
||||
* storage_path('app'). Laravel 12 changed the default local root to
|
||||
* storage_path('app/private'); if that pin were lost (or the framework's
|
||||
* default disks stopped being merged in) every stored icon path would silently
|
||||
* point at the wrong directory. These tests fail loudly if that happens.
|
||||
*/
|
||||
class StorageDiskTest extends TestCase
|
||||
{
|
||||
public function test_local_disk_resolves(): void
|
||||
{
|
||||
$this->assertInstanceOf(Filesystem::class, Storage::disk('local'));
|
||||
}
|
||||
|
||||
public function test_public_disk_resolves(): void
|
||||
{
|
||||
// The public disk comes from the framework defaults merged over the
|
||||
// app's partial config/filesystems.php.
|
||||
$this->assertInstanceOf(Filesystem::class, Storage::disk('public'));
|
||||
}
|
||||
|
||||
public function test_local_disk_root_is_pinned_to_storage_app(): void
|
||||
{
|
||||
$this->assertSame(storage_path('app'), config('filesystems.disks.local.root'));
|
||||
|
||||
// The resolved absolute path must live directly under storage/app,
|
||||
// not the Laravel 12 storage/app/private default.
|
||||
$this->assertSame(storage_path('app/icon.png'), Storage::disk('local')->path('icon.png'));
|
||||
}
|
||||
|
||||
public function test_public_disk_root_is_storage_app_public(): void
|
||||
{
|
||||
$this->assertSame(storage_path('app/public'), config('filesystems.disks.public.root'));
|
||||
}
|
||||
|
||||
public function test_public_disk_supports_a_put_exists_get_round_trip(): void
|
||||
{
|
||||
Storage::fake('public');
|
||||
|
||||
$contents = 'icon-bytes';
|
||||
Storage::disk('public')->put('icons/test.png', $contents);
|
||||
|
||||
$this->assertTrue(Storage::disk('public')->exists('icons/test.png'));
|
||||
$this->assertSame($contents, Storage::disk('public')->get('icons/test.png'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\helpers;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Regression coverage for className() in app/Helper.php.
|
||||
*
|
||||
* className() turns a supported-app display name into the PHP class-name
|
||||
* fragment used to resolve enhanced-app classes (see Application::single()).
|
||||
* It relies on a unicode-aware preg_replace, which is exactly the kind of
|
||||
* PCRE behaviour that can change across PHP versions, so the stripping rules
|
||||
* and unicode-safety are pinned here.
|
||||
*/
|
||||
class ClassNameTest extends TestCase
|
||||
{
|
||||
public function test_strips_spaces_and_punctuation(): void
|
||||
{
|
||||
$this->assertSame('HomeAssistant', className('Home Assistant'));
|
||||
$this->assertSame('Pihole', className('Pi-hole'));
|
||||
$this->assertSame('NodeRED', className('Node-RED!'));
|
||||
}
|
||||
|
||||
public function test_keeps_digits(): void
|
||||
{
|
||||
$this->assertSame('App2Go', className('App 2 Go'));
|
||||
}
|
||||
|
||||
public function test_is_unicode_safe(): void
|
||||
{
|
||||
// Letters in other scripts / accented letters must be preserved,
|
||||
// only the separators are removed.
|
||||
$this->assertSame('CaféServer', className('Café Server'));
|
||||
$this->assertSame('中文测试', className('中文 测试'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\helpers;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Regression coverage for the colour brightness globals in app/Helper.php.
|
||||
*
|
||||
* get_brightness() and title_color() drive the automatic black/white tile
|
||||
* text colour on the dashboard. They rely on hexdec(), substr() and integer
|
||||
* maths, all of which are sensitive to PHP behavioural changes, so the
|
||||
* expected luminance values and the black/white threshold are pinned here.
|
||||
*/
|
||||
class ColorHelpersTest extends TestCase
|
||||
{
|
||||
public function test_get_brightness_returns_full_luminance_for_white(): void
|
||||
{
|
||||
$this->assertEqualsWithDelta(255, get_brightness('#ffffff'), 0.0001);
|
||||
}
|
||||
|
||||
public function test_get_brightness_returns_zero_for_black(): void
|
||||
{
|
||||
$this->assertEqualsWithDelta(0, get_brightness('#000000'), 0.0001);
|
||||
}
|
||||
|
||||
public function test_get_brightness_expands_three_char_hex(): void
|
||||
{
|
||||
// #fff / #000 must expand to the six-char form before decoding.
|
||||
$this->assertEqualsWithDelta(255, get_brightness('#fff'), 0.0001);
|
||||
$this->assertEqualsWithDelta(0, get_brightness('#000'), 0.0001);
|
||||
}
|
||||
|
||||
public function test_get_brightness_strips_leading_hash_and_other_non_hex(): void
|
||||
{
|
||||
// A value without the leading # must decode identically.
|
||||
$this->assertEqualsWithDelta(255, get_brightness('ffffff'), 0.0001);
|
||||
}
|
||||
|
||||
public function test_get_brightness_weights_channels_per_luma_formula(): void
|
||||
{
|
||||
// (R*299 + G*587 + B*114) / 1000
|
||||
$this->assertEqualsWithDelta(76.245, get_brightness('#ff0000'), 0.0001);
|
||||
$this->assertEqualsWithDelta(149.685, get_brightness('#00ff00'), 0.0001);
|
||||
$this->assertEqualsWithDelta(29.07, get_brightness('#0000ff'), 0.0001);
|
||||
}
|
||||
|
||||
public function test_title_color_returns_black_for_bright_colours(): void
|
||||
{
|
||||
// Brightness > 130 => dark text.
|
||||
$this->assertSame(' black', title_color('#ffffff'));
|
||||
$this->assertSame(' black', title_color('#00ff00'));
|
||||
}
|
||||
|
||||
public function test_title_color_returns_white_for_dark_colours(): void
|
||||
{
|
||||
// Brightness <= 130 => light text.
|
||||
$this->assertSame(' white', title_color('#000000'));
|
||||
$this->assertSame(' white', title_color('#0000ff'));
|
||||
$this->assertSame(' white', title_color('#ff0000'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\helpers;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Regression coverage for the byte / size formatting globals in app/Helper.php.
|
||||
*
|
||||
* These are plain global functions (no framework involved) so a PHP upgrade
|
||||
* that changes integer/float division, rounding, or numeric-string casting is
|
||||
* exactly the kind of thing that would silently break them. Every assertion
|
||||
* pins a concrete expected string/integer so the behaviour is locked down.
|
||||
*/
|
||||
class SizeHelpersTest extends TestCase
|
||||
{
|
||||
public function test_format_bytes_uses_drive_size_base_1000_by_default(): void
|
||||
{
|
||||
// Default $is_drive_size = true => divide by 1000 (simulated HD size).
|
||||
$this->assertSame('500B', format_bytes(500));
|
||||
$this->assertSame('1KB', format_bytes(1000));
|
||||
$this->assertSame('2KB', format_bytes(2000));
|
||||
$this->assertSame('1MB', format_bytes(1000000));
|
||||
$this->assertSame('1.5MB', format_bytes(1500000));
|
||||
$this->assertSame('1GB', format_bytes(1000000000));
|
||||
$this->assertSame('2.5GB', format_bytes(2500000000));
|
||||
$this->assertSame('1TB', format_bytes(1000000000000));
|
||||
}
|
||||
|
||||
public function test_format_bytes_base_1024_when_not_drive_size(): void
|
||||
{
|
||||
// $is_drive_size = false => divide by 1024 (real byte size).
|
||||
$this->assertSame('1KB', format_bytes(1024, false));
|
||||
$this->assertSame('1MB', format_bytes(1048576, false));
|
||||
$this->assertSame('1GB', format_bytes(1073741824, false));
|
||||
$this->assertSame('1.43MB', format_bytes(1500000, false));
|
||||
}
|
||||
|
||||
public function test_format_bytes_drive_size_flag_changes_the_result(): void
|
||||
{
|
||||
// The same byte count must format differently depending on the base.
|
||||
$this->assertSame('1MB', format_bytes(1000000, true));
|
||||
$this->assertSame('977KB', format_bytes(1000000, false));
|
||||
}
|
||||
|
||||
public function test_format_bytes_caps_at_terabytes(): void
|
||||
{
|
||||
// The unit loop stops at TB (index 4) even for very large inputs.
|
||||
$this->assertSame('5TB', format_bytes(5000000000000));
|
||||
}
|
||||
|
||||
public function test_format_bytes_applies_before_and_after_unit_strings(): void
|
||||
{
|
||||
$this->assertSame('1 KBps', format_bytes(1000, true, ' ', 'ps'));
|
||||
$this->assertSame('1.43 MB/s', format_bytes(1500000, false, ' ', '/s'));
|
||||
}
|
||||
|
||||
public function test_parse_size_resolves_gmk_suffixes_to_bytes(): void
|
||||
{
|
||||
$this->assertSame(1073741824, parse_size('1g'));
|
||||
$this->assertSame(2147483648, parse_size('2G'));
|
||||
$this->assertSame(536870912, parse_size('512m'));
|
||||
$this->assertSame(131072, parse_size('128k'));
|
||||
}
|
||||
|
||||
public function test_parse_size_without_suffix_returns_the_integer_value(): void
|
||||
{
|
||||
$this->assertSame(1024, parse_size('1024'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user