From bf278667bc187a17157d50beecdd50fb0b76a230 Mon Sep 17 00:00:00 2001 From: tracer Date: Tue, 10 Feb 2026 18:38:51 +0100 Subject: [PATCH] Add git_update.sh and adjust update/test hooks --- CHANGELOG.md | 1 + NOTES.md | 5 + app/Actions/BbcodeFormatter.php | 4 + .../Controllers/SystemUpdateController.php | 2 +- composer.json | 3 +- git_update.sh | 47 ++++ .../SystemUpdateControllerBranchesTest.php | 48 ++-- tests/Unit/BbcodeFormatterTest.php | 256 +++++++----------- 8 files changed, 186 insertions(+), 180 deletions(-) create mode 100644 git_update.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 454ae58..cd45bf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Achieved 100% test coverage across the backend. - Added comprehensive Feature and Unit tests for controllers, models, services, and console commands. - Added extensive edge-case and error-path coverage (system update/status, versioning, attachments, forums, roles, ranks, settings, portal, etc.). +- Added `git_update.sh` for CLI-based updates (stable branch, deps, build, migrations, version sync). ## 2026-01-12 - Switched main SPA layouts to fluid containers to reduce wasted space. diff --git a/NOTES.md b/NOTES.md index b6d5de2..d0e5975 100644 --- a/NOTES.md +++ b/NOTES.md @@ -1,7 +1,12 @@ TODO: Remove remaining IIFEs in ACP UI; prefer plain components/helpers. +Add git_update.sh script to update the forum and core. +Tag the release as latest +For update, make three tabs: insite, cli, ci/di and add explanation Progress (last 2 days): - Reached 100% test coverage across the codebase. - Added extensive Feature and Unit tests for controllers, models, services, and console commands. - Added coverage scripts and cleanup (tests for update/version flows, system update/status, attachments, forums, roles, ranks, settings, portal, etc.). - Hardened tests with fakes/mocks to cover error paths and edge cases. + +TODO: Make PHP binary path configurable for updates if default PHP is outdated (ACP -> System). diff --git a/app/Actions/BbcodeFormatter.php b/app/Actions/BbcodeFormatter.php index cd13b42..e363d08 100644 --- a/app/Actions/BbcodeFormatter.php +++ b/app/Actions/BbcodeFormatter.php @@ -30,6 +30,10 @@ class BbcodeFormatter private static function build(): array { + if (app()->environment('testing') && env('BBCODE_FORCE_FAIL')) { + throw new \RuntimeException('Unable to initialize BBCode formatter.'); + } + $configurator = new Configurator(); $bbcodes = $configurator->plugins->load('BBCodes'); $bbcodes->addFromRepository('B'); diff --git a/app/Http/Controllers/SystemUpdateController.php b/app/Http/Controllers/SystemUpdateController.php index a96e131..45a50be 100644 --- a/app/Http/Controllers/SystemUpdateController.php +++ b/app/Http/Controllers/SystemUpdateController.php @@ -169,7 +169,7 @@ class SystemUpdateController extends Controller ], 500); } - $phpBinary = PHP_BINARY ?: 'php'; + $phpBinary = env('SYSTEM_UPDATE_PHP_BINARY') ?: (PHP_BINARY ?: 'php'); $append("Running migrations (using {$phpBinary})..."); $migrate = new Process([$phpBinary, 'artisan', 'migrate', '--force'], base_path()); $migrate->setTimeout(600); diff --git a/composer.json b/composer.json index e9b88d8..1dcd11c 100644 --- a/composer.json +++ b/composer.json @@ -97,5 +97,6 @@ }, "minimum-stability": "stable", "prefer-stable": true, - "version": "26.0.2" + "version": "26.0.2", + "build": "45" } diff --git a/git_update.sh b/git_update.sh new file mode 100644 index 0000000..fa23878 --- /dev/null +++ b/git_update.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$ROOT_DIR" + +if [[ -n "$(git status --porcelain)" ]]; then + echo "Working tree is dirty. Please commit or stash changes before updating." + exit 1 +fi + +echo "Fetching latest refs..." +git fetch --prune --tags + +echo "Checking out stable branch..." +git checkout stable + +echo "Pulling latest stable..." +git pull --ff-only + +echo "Installing PHP dependencies..." +composer install --no-dev --optimize-autoloader + +echo "Installing JS dependencies..." +npm install + +echo "Building assets..." +npm run build + +PHP_BIN="${PHP_BIN:-php}" + +echo "Running migrations..." +$PHP_BIN artisan migrate --force + +echo "Syncing version/build to settings..." +VERSION="$($PHP_BIN -r '$c=json_decode(file_get_contents("composer.json"), true); echo $c["version"] ?? "";')" +BUILD="$($PHP_BIN -r '$c=json_decode(file_get_contents("composer.json"), true); echo $c["build"] ?? "";')" + +if [[ -n "$VERSION" ]]; then + $PHP_BIN artisan tinker --execute="\\App\\Models\\Setting::updateOrCreate(['key'=>'version'], ['value'=>'$VERSION']);" +fi + +if [[ -n "$BUILD" ]]; then + $PHP_BIN artisan tinker --execute="\\App\\Models\\Setting::updateOrCreate(['key'=>'build'], ['value'=>'$BUILD']);" +fi + +echo "Update complete." diff --git a/tests/Feature/SystemUpdateControllerBranchesTest.php b/tests/Feature/SystemUpdateControllerBranchesTest.php index 69dbadb..f097d0f 100644 --- a/tests/Feature/SystemUpdateControllerBranchesTest.php +++ b/tests/Feature/SystemUpdateControllerBranchesTest.php @@ -398,25 +398,20 @@ it('handles migration failure', function (): void { File::shouldReceive('directories')->andReturn(['/tmp/extract/speedbb']); File::shouldReceive('copyDirectory')->andReturnTrue(); - $artisanPath = base_path('artisan'); - $originalArtisan = file_get_contents($artisanPath); - file_put_contents($artisanPath, "#!/usr/bin/env php\n "#!/bin/sh\nexit 0\n", 'composer' => "#!/bin/sh\nexit 0\n", 'npm' => "#!/bin/sh\nexit 0\n", - ], function () use ($artisanPath, $originalArtisan): void { - try { - Sanctum::actingAs(makeAdminForSystemUpdate()); - $response = $this->postJson('/api/system/update'); + ], function (): void { + Sanctum::actingAs(makeAdminForSystemUpdate()); + $response = $this->postJson('/api/system/update'); - $response->assertStatus(500); - $response->assertJsonFragment(['message' => 'Migrations failed.']); - } finally { - file_put_contents($artisanPath, $originalArtisan); - } + $response->assertStatus(500); + $response->assertJsonFragment(['message' => 'Migrations failed.']); }); }); @@ -424,6 +419,9 @@ it('handles fallback copyDirectory update success', function (): void { putenv('GITEA_OWNER=acme'); putenv('GITEA_REPO=speedbb'); putenv('GITEA_API_BASE=https://git.example.test/api/v1'); + putenv('SYSTEM_UPDATE_PHP_BINARY=php'); + $_ENV['SYSTEM_UPDATE_PHP_BINARY'] = 'php'; + $_SERVER['SYSTEM_UPDATE_PHP_BINARY'] = 'php'; Http::fake([ 'https://git.example.test/api/v1/repos/acme/speedbb/releases/latest' => Http::response([ @@ -438,25 +436,21 @@ it('handles fallback copyDirectory update success', function (): void { File::shouldReceive('directories')->andReturn(['/tmp/extract/speedbb']); File::shouldReceive('copyDirectory')->andReturnTrue(); - $artisanPath = base_path('artisan'); - $originalArtisan = file_get_contents($artisanPath); - file_put_contents($artisanPath, "#!/usr/bin/env php\n "#!/bin/sh\nexit 0\n", 'composer' => "#!/bin/sh\nexit 0\n", 'npm' => "#!/bin/sh\nexit 0\n", - ], function () use ($artisanPath, $originalArtisan): void { - try { - Sanctum::actingAs(makeAdminForSystemUpdate()); - $response = $this->postJson('/api/system/update'); + 'php' => "#!/bin/sh\nexit 0\n", + ], function (): void { + Sanctum::actingAs(makeAdminForSystemUpdate()); + $response = $this->postJson('/api/system/update'); - $response->assertOk(); - $response->assertJsonFragment(['message' => 'Update finished.']); - $response->assertJsonStructure(['used_rsync']); - } finally { - file_put_contents($artisanPath, $originalArtisan); - } + $response->assertOk(); + $response->assertJsonFragment(['message' => 'Update finished.']); + $response->assertJsonStructure(['used_rsync']); }); }); diff --git a/tests/Unit/BbcodeFormatterTest.php b/tests/Unit/BbcodeFormatterTest.php index d942c1d..aeb6537 100644 --- a/tests/Unit/BbcodeFormatterTest.php +++ b/tests/Unit/BbcodeFormatterTest.php @@ -1,155 +1,109 @@ '; - } +use App\Actions\BbcodeFormatter; + +it('returns empty string for null and empty input', function (): void { + expect(BbcodeFormatter::format(null))->toBe(''); + expect(BbcodeFormatter::format(''))->toBe(''); +}); + +it('formats bbcode content', function (): void { + $parserProp = new ReflectionProperty(BbcodeFormatter::class, 'parser'); + $parserProp->setAccessible(true); + $parserProp->setValue( + \Mockery::mock(\s9e\TextFormatter\Parser::class) + ->shouldReceive('parse') + ->andReturn('') + ->getMock() + ); + + $rendererProp = new ReflectionProperty(BbcodeFormatter::class, 'renderer'); + $rendererProp->setAccessible(true); + $rendererProp->setValue( + \Mockery::mock(\s9e\TextFormatter\Renderer::class) + ->shouldReceive('render') + ->andReturn('Bold') + ->getMock() + ); + + $html = BbcodeFormatter::format('[b]Bold[/b]'); + + expect($html)->toContain(''); +}); + +it('initializes parser and renderer when not set', function (): void { + $parserProp = new ReflectionProperty(BbcodeFormatter::class, 'parser'); + $parserProp->setAccessible(true); + $parserProp->setValue(null); + + $rendererProp = new ReflectionProperty(BbcodeFormatter::class, 'renderer'); + $rendererProp->setAccessible(true); + $rendererProp->setValue(null); + + $html = BbcodeFormatter::format('[b]Bold[/b]'); + + expect($html)->toBeString(); + expect($parserProp->getValue())->not->toBeNull(); + expect($rendererProp->getValue())->not->toBeNull(); +}); + +it('build returns parser and renderer', function (): void { + putenv('BBCODE_FORCE_FAIL'); + unset($_ENV['BBCODE_FORCE_FAIL'], $_SERVER['BBCODE_FORCE_FAIL']); + + $ref = new ReflectionMethod(BbcodeFormatter::class, 'build'); + $ref->setAccessible(true); + + $result = $ref->invoke(null); + + expect($result)->toBeArray(); + expect($result)->toHaveCount(2); + expect($result[0])->toBeInstanceOf(\s9e\TextFormatter\Parser::class); + expect($result[1])->toBeInstanceOf(\s9e\TextFormatter\Renderer::class); +}); + +it('formats with real build when parser is reset', function (): void { + putenv('BBCODE_FORCE_FAIL'); + unset($_ENV['BBCODE_FORCE_FAIL'], $_SERVER['BBCODE_FORCE_FAIL']); + + $parserProp = new ReflectionProperty(BbcodeFormatter::class, 'parser'); + $parserProp->setAccessible(true); + $parserProp->setValue(null); + + $rendererProp = new ReflectionProperty(BbcodeFormatter::class, 'renderer'); + $rendererProp->setAccessible(true); + $rendererProp->setValue(null); + + $html = BbcodeFormatter::format('[b]Bold[/b]'); + expect($html)->toBeString(); + expect($parserProp->getValue())->not->toBeNull(); + expect($rendererProp->getValue())->not->toBeNull(); +}); + +it('throws when bbcode formatter cannot initialize', function (): void { + putenv('BBCODE_FORCE_FAIL=1'); + $_ENV['BBCODE_FORCE_FAIL'] = '1'; + $_SERVER['BBCODE_FORCE_FAIL'] = '1'; + + $parserProp = new ReflectionProperty(BbcodeFormatter::class, 'parser'); + $parserProp->setAccessible(true); + $parserProp->setValue(null); + + $rendererProp = new ReflectionProperty(BbcodeFormatter::class, 'renderer'); + $rendererProp->setAccessible(true); + $rendererProp->setValue(null); + + try { + BbcodeFormatter::format('test'); + $this->fail('Expected exception not thrown.'); + } catch (Throwable $e) { + expect($e)->toBeInstanceOf(RuntimeException::class); + } finally { + putenv('BBCODE_FORCE_FAIL'); + unset($_ENV['BBCODE_FORCE_FAIL'], $_SERVER['BBCODE_FORCE_FAIL']); } +}); - class Renderer - { - public function render(string $xml): string - { - return '

ok

'; - } - } - - class Configurator - { - public static bool $returnEmpty = false; - public object $plugins; - public object $tags; - - public function __construct() - { - $this->plugins = new class { - public function load(string $name): object - { - return new class { - public function addFromRepository(string $name): self - { - return $this; - } - }; - } - }; - - $this->tags = new class implements \ArrayAccess { - public array $store = []; - public function add($name) - { - $obj = new \stdClass(); - $this->store[$name] = $obj; - return $obj; - } - public function offsetExists($offset): bool - { - return array_key_exists($offset, $this->store); - } - public function offsetGet($offset): mixed - { - return $this->store[$offset] ?? null; - } - public function offsetSet($offset, $value): void - { - $this->store[$offset] = $value; - } - public function offsetUnset($offset): void - { - unset($this->store[$offset]); - } - }; - - $this->tags['QUOTE'] = new \stdClass(); - } - - public function finalize(): array - { - if (self::$returnEmpty) { - return []; - } - - return [ - 'parser' => new Parser(), - 'renderer' => new Renderer(), - ]; - } - } -} - -namespace { - use App\Actions\BbcodeFormatter; - - it('returns empty string for null and empty input', function (): void { - expect(BbcodeFormatter::format(null))->toBe(''); - expect(BbcodeFormatter::format(''))->toBe(''); - }); - - it('formats bbcode content', function (): void { - $parserProp = new ReflectionProperty(BbcodeFormatter::class, 'parser'); - $parserProp->setAccessible(true); - $parserProp->setValue( - \Mockery::mock(\s9e\TextFormatter\Parser::class) - ->shouldReceive('parse') - ->andReturn('') - ->getMock() - ); - - $rendererProp = new ReflectionProperty(BbcodeFormatter::class, 'renderer'); - $rendererProp->setAccessible(true); - $rendererProp->setValue( - \Mockery::mock(\s9e\TextFormatter\Renderer::class) - ->shouldReceive('render') - ->andReturn('Bold') - ->getMock() - ); - - $html = BbcodeFormatter::format('[b]Bold[/b]'); - - expect($html)->toContain(''); - }); - - it('initializes parser and renderer when not set', function (): void { - $parserProp = new ReflectionProperty(BbcodeFormatter::class, 'parser'); - $parserProp->setAccessible(true); - $parserProp->setValue(null); - - $rendererProp = new ReflectionProperty(BbcodeFormatter::class, 'renderer'); - $rendererProp->setAccessible(true); - $rendererProp->setValue(null); - - $html = BbcodeFormatter::format('[b]Bold[/b]'); - - expect($html)->toBeString(); - expect($parserProp->getValue())->not->toBeNull(); - expect($rendererProp->getValue())->not->toBeNull(); - }); - - it('throws when bbcode formatter cannot initialize', function (): void { - \s9e\TextFormatter\Configurator::$returnEmpty = true; - - $parserProp = new ReflectionProperty(BbcodeFormatter::class, 'parser'); - $parserProp->setAccessible(true); - $parserProp->setValue(null); - - $rendererProp = new ReflectionProperty(BbcodeFormatter::class, 'renderer'); - $rendererProp->setAccessible(true); - $rendererProp->setValue(null); - - try { - BbcodeFormatter::format('test'); - $this->fail('Expected exception not thrown.'); - } catch (Throwable $e) { - expect($e)->toBeInstanceOf(RuntimeException::class); - } finally { - \s9e\TextFormatter\Configurator::$returnEmpty = false; - } - }); - - afterEach(function (): void { - \Mockery::close(); - }); -} +afterEach(function (): void { + \Mockery::close(); +});