Compare commits
8 Commits
v26.0.2
...
af03c23c9f
| Author | SHA1 | Date | |
|---|---|---|---|
| af03c23c9f | |||
| 68dd17f895 | |||
| 8249df15df | |||
| f167e64d00 | |||
| 95ebc7778d | |||
| c67a3ec6d0 | |||
| bf278667bc | |||
| 30a06e18f0 |
@@ -37,3 +37,24 @@ jobs:
|
|||||||
echo "$ANSIBLE_VAULT_PASSWORD" > .vault_pass.txt
|
echo "$ANSIBLE_VAULT_PASSWORD" > .vault_pass.txt
|
||||||
ansible-playbook --vault-password-file .vault_pass.txt deploy-to-prod.yaml
|
ansible-playbook --vault-password-file .vault_pass.txt deploy-to-prod.yaml
|
||||||
rm .vault_pass.txt
|
rm .vault_pass.txt
|
||||||
|
|
||||||
|
promote_stable:
|
||||||
|
runs-on: self-hosted
|
||||||
|
needs: deploy
|
||||||
|
steps:
|
||||||
|
- name: Promote master to stable
|
||||||
|
env:
|
||||||
|
SPEEDBB_REPO: ${{ vars.SPEEDBB_REPO }}
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
GITEA_ACTOR: ${{ gitea.actor }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
REPO="$SPEEDBB_REPO"
|
||||||
|
if [ -n "$GITEA_TOKEN" ]; then
|
||||||
|
REPO=$(echo "$SPEEDBB_REPO" | sed "s#https://#https://${GITEA_ACTOR}:${GITEA_TOKEN}@#")
|
||||||
|
fi
|
||||||
|
git clone --quiet --depth=1 --branch=stable "$REPO" repo
|
||||||
|
cd repo
|
||||||
|
git fetch origin master
|
||||||
|
git merge --ff-only FETCH_HEAD
|
||||||
|
git push origin stable
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-10
|
||||||
|
- Reshaped ACP System tab with left navigation and dedicated views (Overview, Live Update, CLI, CI/CD).
|
||||||
|
- Moved system requirements table into the CI/CD view with refresh controls.
|
||||||
|
|
||||||
## 2026-02-08
|
## 2026-02-08
|
||||||
- Achieved 100% test coverage across the backend.
|
- Achieved 100% test coverage across the backend.
|
||||||
- Added comprehensive Feature and Unit tests for controllers, models, services, and console commands.
|
- 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 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
|
## 2026-01-12
|
||||||
- Switched main SPA layouts to fluid containers to reduce wasted space.
|
- Switched main SPA layouts to fluid containers to reduce wasted space.
|
||||||
|
|||||||
5
NOTES.md
5
NOTES.md
@@ -1,7 +1,12 @@
|
|||||||
TODO: Remove remaining IIFEs in ACP UI; prefer plain components/helpers.
|
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):
|
Progress (last 2 days):
|
||||||
- Reached 100% test coverage across the codebase.
|
- Reached 100% test coverage across the codebase.
|
||||||
- Added extensive Feature and Unit tests for controllers, models, services, and console commands.
|
- 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.).
|
- 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.
|
- 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).
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ class BbcodeFormatter
|
|||||||
|
|
||||||
private static function build(): array
|
private static function build(): array
|
||||||
{
|
{
|
||||||
|
if (app()->environment('testing') && env('BBCODE_FORCE_FAIL')) {
|
||||||
|
throw new \RuntimeException('Unable to initialize BBCode formatter.');
|
||||||
|
}
|
||||||
|
|
||||||
$configurator = new Configurator();
|
$configurator = new Configurator();
|
||||||
$bbcodes = $configurator->plugins->load('BBCodes');
|
$bbcodes = $configurator->plugins->load('BBCodes');
|
||||||
$bbcodes->addFromRepository('B');
|
$bbcodes->addFromRepository('B');
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ class SystemUpdateController extends Controller
|
|||||||
], 500);
|
], 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
$phpBinary = PHP_BINARY ?: 'php';
|
$phpBinary = env('SYSTEM_UPDATE_PHP_BINARY') ?: (PHP_BINARY ?: 'php');
|
||||||
$append("Running migrations (using {$phpBinary})...");
|
$append("Running migrations (using {$phpBinary})...");
|
||||||
$migrate = new Process([$phpBinary, 'artisan', 'migrate', '--force'], base_path());
|
$migrate = new Process([$phpBinary, 'artisan', 'migrate', '--force'], base_path());
|
||||||
$migrate->setTimeout(600);
|
$migrate->setTimeout(600);
|
||||||
|
|||||||
@@ -97,5 +97,6 @@
|
|||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
"version": "26.0.1"
|
"version": "26.0.2",
|
||||||
|
"build": "49"
|
||||||
}
|
}
|
||||||
|
|||||||
47
git_update.sh
Executable file
47
git_update.sh
Executable file
@@ -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."
|
||||||
@@ -96,6 +96,7 @@ function Acp({ isAdmin }) {
|
|||||||
const [systemStatus, setSystemStatus] = useState(null)
|
const [systemStatus, setSystemStatus] = useState(null)
|
||||||
const [systemLoading, setSystemLoading] = useState(false)
|
const [systemLoading, setSystemLoading] = useState(false)
|
||||||
const [systemError, setSystemError] = useState('')
|
const [systemError, setSystemError] = useState('')
|
||||||
|
const [systemSection, setSystemSection] = useState('info')
|
||||||
const [usersPage, setUsersPage] = useState(1)
|
const [usersPage, setUsersPage] = useState(1)
|
||||||
const [usersPerPage, setUsersPerPage] = useState(10)
|
const [usersPerPage, setUsersPerPage] = useState(10)
|
||||||
const [userSort, setUserSort] = useState({ columnId: 'name', direction: 'asc' })
|
const [userSort, setUserSort] = useState({ columnId: 'name', direction: 'asc' })
|
||||||
@@ -3507,7 +3508,93 @@ function Acp({ isAdmin }) {
|
|||||||
<Tab eventKey="system" title={t('acp.system')}>
|
<Tab eventKey="system" title={t('acp.system')}>
|
||||||
{systemError && <p className="text-danger">{systemError}</p>}
|
{systemError && <p className="text-danger">{systemError}</p>}
|
||||||
{systemLoading && <p className="bb-muted">{t('acp.loading')}</p>}
|
{systemLoading && <p className="bb-muted">{t('acp.loading')}</p>}
|
||||||
{!systemLoading && systemStatus && (
|
{!systemLoading && (
|
||||||
|
<Row className="g-4">
|
||||||
|
<Col lg={3} xl={2}>
|
||||||
|
<div className="bb-acp-sidebar">
|
||||||
|
<div className="bb-acp-sidebar-section">
|
||||||
|
<div className="bb-acp-sidebar-title">{t('acp.system')}</div>
|
||||||
|
<div className="list-group">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`list-group-item list-group-item-action ${
|
||||||
|
systemSection === 'info' ? 'is-active' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => setSystemSection('info')}
|
||||||
|
>
|
||||||
|
Overview
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`list-group-item list-group-item-action ${
|
||||||
|
systemSection === 'insite' ? 'is-active' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => setSystemSection('insite')}
|
||||||
|
>
|
||||||
|
Live Update
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`list-group-item list-group-item-action ${
|
||||||
|
systemSection === 'cli' ? 'is-active' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => setSystemSection('cli')}
|
||||||
|
>
|
||||||
|
CLI
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`list-group-item list-group-item-action ${
|
||||||
|
systemSection === 'ci' ? 'is-active' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => setSystemSection('ci')}
|
||||||
|
>
|
||||||
|
CI/CD
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col lg={9} xl={10}>
|
||||||
|
{systemSection === 'info' && (
|
||||||
|
<div className="bb-acp-panel">
|
||||||
|
<div className="bb-acp-panel-header">
|
||||||
|
<h5 className="mb-0">System overview</h5>
|
||||||
|
</div>
|
||||||
|
<div className="bb-acp-panel-body">
|
||||||
|
<p className="bb-muted mb-0">
|
||||||
|
Placeholder: summary, upgrade guidance, and environment health notes will
|
||||||
|
live here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{systemSection === 'insite' && (
|
||||||
|
<div className="bb-acp-panel">
|
||||||
|
<div className="bb-acp-panel-header">
|
||||||
|
<h5 className="mb-0">Live Update</h5>
|
||||||
|
</div>
|
||||||
|
<div className="bb-acp-panel-body">
|
||||||
|
<p className="bb-muted mb-0">
|
||||||
|
Placeholder: run a live update from inside the forum, with safety checks
|
||||||
|
and status details.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{systemSection === 'cli' && (
|
||||||
|
<div className="bb-acp-panel">
|
||||||
|
<div className="bb-acp-panel-header">
|
||||||
|
<h5 className="mb-0">CLI</h5>
|
||||||
|
</div>
|
||||||
|
<div className="bb-acp-panel-body">
|
||||||
|
<p className="bb-muted mb-0">
|
||||||
|
Placeholder: CLI upgrade commands and automation helpers will live here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{systemSection === 'ci' && (
|
||||||
<div className="bb-acp-panel">
|
<div className="bb-acp-panel">
|
||||||
<div className="bb-acp-panel-header">
|
<div className="bb-acp-panel-header">
|
||||||
<div className="d-flex align-items-center justify-content-between">
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
@@ -3524,6 +3611,12 @@ function Acp({ isAdmin }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bb-acp-panel-body">
|
<div className="bb-acp-panel-body">
|
||||||
|
{!systemStatus && (
|
||||||
|
<p className="bb-muted mb-0">
|
||||||
|
{t('system.not_found')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{systemStatus && (
|
||||||
<table className="bb-acp-stats-table">
|
<table className="bb-acp-stats-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -3538,7 +3631,9 @@ function Acp({ isAdmin }) {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>PHP</td>
|
<td>PHP</td>
|
||||||
<td className="bb-acp-stats-value">{systemStatus.php_selected_path || '—'}</td>
|
<td className="bb-acp-stats-value">
|
||||||
|
{systemStatus.php_selected_path || '—'}
|
||||||
|
</td>
|
||||||
<td className="bb-acp-stats-value">
|
<td className="bb-acp-stats-value">
|
||||||
{systemStatus.min_versions?.php || '—'}
|
{systemStatus.min_versions?.php || '—'}
|
||||||
</td>
|
</td>
|
||||||
@@ -3753,9 +3848,13 @@ function Acp({ isAdmin }) {
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Modal show={showModal} onHide={handleReset} centered size="lg">
|
<Modal show={showModal} onHide={handleReset} centered size="lg">
|
||||||
|
|||||||
5
scripts/hooks/pre-commit
Normal file
5
scripts/hooks/pre-commit
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Fail fast if the database is unreachable.
|
||||||
|
php artisan version:fetch >/dev/null
|
||||||
@@ -398,25 +398,20 @@ it('handles migration failure', function (): void {
|
|||||||
File::shouldReceive('directories')->andReturn(['/tmp/extract/speedbb']);
|
File::shouldReceive('directories')->andReturn(['/tmp/extract/speedbb']);
|
||||||
File::shouldReceive('copyDirectory')->andReturnTrue();
|
File::shouldReceive('copyDirectory')->andReturnTrue();
|
||||||
|
|
||||||
$artisanPath = base_path('artisan');
|
putenv('SYSTEM_UPDATE_PHP_BINARY=/nope');
|
||||||
$originalArtisan = file_get_contents($artisanPath);
|
$_ENV['SYSTEM_UPDATE_PHP_BINARY'] = '/nope';
|
||||||
file_put_contents($artisanPath, "#!/usr/bin/env php\n<?php exit(1);\n");
|
$_SERVER['SYSTEM_UPDATE_PHP_BINARY'] = '/nope';
|
||||||
chmod($artisanPath, 0755);
|
|
||||||
|
|
||||||
withFakeBin([
|
withFakeBin([
|
||||||
'tar' => "#!/bin/sh\nexit 0\n",
|
'tar' => "#!/bin/sh\nexit 0\n",
|
||||||
'composer' => "#!/bin/sh\nexit 0\n",
|
'composer' => "#!/bin/sh\nexit 0\n",
|
||||||
'npm' => "#!/bin/sh\nexit 0\n",
|
'npm' => "#!/bin/sh\nexit 0\n",
|
||||||
], function () use ($artisanPath, $originalArtisan): void {
|
], function (): void {
|
||||||
try {
|
|
||||||
Sanctum::actingAs(makeAdminForSystemUpdate());
|
Sanctum::actingAs(makeAdminForSystemUpdate());
|
||||||
$response = $this->postJson('/api/system/update');
|
$response = $this->postJson('/api/system/update');
|
||||||
|
|
||||||
$response->assertStatus(500);
|
$response->assertStatus(500);
|
||||||
$response->assertJsonFragment(['message' => 'Migrations failed.']);
|
$response->assertJsonFragment(['message' => 'Migrations failed.']);
|
||||||
} finally {
|
|
||||||
file_put_contents($artisanPath, $originalArtisan);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -424,6 +419,9 @@ it('handles fallback copyDirectory update success', function (): void {
|
|||||||
putenv('GITEA_OWNER=acme');
|
putenv('GITEA_OWNER=acme');
|
||||||
putenv('GITEA_REPO=speedbb');
|
putenv('GITEA_REPO=speedbb');
|
||||||
putenv('GITEA_API_BASE=https://git.example.test/api/v1');
|
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([
|
Http::fake([
|
||||||
'https://git.example.test/api/v1/repos/acme/speedbb/releases/latest' => Http::response([
|
'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('directories')->andReturn(['/tmp/extract/speedbb']);
|
||||||
File::shouldReceive('copyDirectory')->andReturnTrue();
|
File::shouldReceive('copyDirectory')->andReturnTrue();
|
||||||
|
|
||||||
$artisanPath = base_path('artisan');
|
putenv('SYSTEM_UPDATE_PHP_BINARY=php');
|
||||||
$originalArtisan = file_get_contents($artisanPath);
|
$_ENV['SYSTEM_UPDATE_PHP_BINARY'] = 'php';
|
||||||
file_put_contents($artisanPath, "#!/usr/bin/env php\n<?php exit(0);\n");
|
$_SERVER['SYSTEM_UPDATE_PHP_BINARY'] = 'php';
|
||||||
chmod($artisanPath, 0755);
|
|
||||||
|
|
||||||
withFakeBin([
|
withFakeBin([
|
||||||
'tar' => "#!/bin/sh\nexit 0\n",
|
'tar' => "#!/bin/sh\nexit 0\n",
|
||||||
'composer' => "#!/bin/sh\nexit 0\n",
|
'composer' => "#!/bin/sh\nexit 0\n",
|
||||||
'npm' => "#!/bin/sh\nexit 0\n",
|
'npm' => "#!/bin/sh\nexit 0\n",
|
||||||
], function () use ($artisanPath, $originalArtisan): void {
|
'php' => "#!/bin/sh\nexit 0\n",
|
||||||
try {
|
], function (): void {
|
||||||
Sanctum::actingAs(makeAdminForSystemUpdate());
|
Sanctum::actingAs(makeAdminForSystemUpdate());
|
||||||
$response = $this->postJson('/api/system/update');
|
$response = $this->postJson('/api/system/update');
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertJsonFragment(['message' => 'Update finished.']);
|
$response->assertJsonFragment(['message' => 'Update finished.']);
|
||||||
$response->assertJsonStructure(['used_rsync']);
|
$response->assertJsonStructure(['used_rsync']);
|
||||||
} finally {
|
|
||||||
file_put_contents($artisanPath, $originalArtisan);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,94 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace s9e\TextFormatter {
|
use App\Actions\BbcodeFormatter;
|
||||||
class Parser
|
|
||||||
{
|
|
||||||
public function parse(string $text): string
|
|
||||||
{
|
|
||||||
return '<r/>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Renderer
|
it('returns empty string for null and empty input', function (): void {
|
||||||
{
|
|
||||||
public function render(string $xml): string
|
|
||||||
{
|
|
||||||
return '<p>ok</p>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(null))->toBe('');
|
||||||
expect(BbcodeFormatter::format(''))->toBe('');
|
expect(BbcodeFormatter::format(''))->toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats bbcode content', function (): void {
|
it('formats bbcode content', function (): void {
|
||||||
$parserProp = new ReflectionProperty(BbcodeFormatter::class, 'parser');
|
$parserProp = new ReflectionProperty(BbcodeFormatter::class, 'parser');
|
||||||
$parserProp->setAccessible(true);
|
$parserProp->setAccessible(true);
|
||||||
$parserProp->setValue(
|
$parserProp->setValue(
|
||||||
@@ -110,9 +29,9 @@ namespace {
|
|||||||
$html = BbcodeFormatter::format('[b]Bold[/b]');
|
$html = BbcodeFormatter::format('[b]Bold[/b]');
|
||||||
|
|
||||||
expect($html)->toContain('<b>');
|
expect($html)->toContain('<b>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('initializes parser and renderer when not set', function (): void {
|
it('initializes parser and renderer when not set', function (): void {
|
||||||
$parserProp = new ReflectionProperty(BbcodeFormatter::class, 'parser');
|
$parserProp = new ReflectionProperty(BbcodeFormatter::class, 'parser');
|
||||||
$parserProp->setAccessible(true);
|
$parserProp->setAccessible(true);
|
||||||
$parserProp->setValue(null);
|
$parserProp->setValue(null);
|
||||||
@@ -126,10 +45,45 @@ namespace {
|
|||||||
expect($html)->toBeString();
|
expect($html)->toBeString();
|
||||||
expect($parserProp->getValue())->not->toBeNull();
|
expect($parserProp->getValue())->not->toBeNull();
|
||||||
expect($rendererProp->getValue())->not->toBeNull();
|
expect($rendererProp->getValue())->not->toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws when bbcode formatter cannot initialize', function (): void {
|
it('build returns parser and renderer', function (): void {
|
||||||
\s9e\TextFormatter\Configurator::$returnEmpty = true;
|
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 = new ReflectionProperty(BbcodeFormatter::class, 'parser');
|
||||||
$parserProp->setAccessible(true);
|
$parserProp->setAccessible(true);
|
||||||
@@ -145,11 +99,11 @@ namespace {
|
|||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
expect($e)->toBeInstanceOf(RuntimeException::class);
|
expect($e)->toBeInstanceOf(RuntimeException::class);
|
||||||
} finally {
|
} finally {
|
||||||
\s9e\TextFormatter\Configurator::$returnEmpty = false;
|
putenv('BBCODE_FORCE_FAIL');
|
||||||
|
unset($_ENV['BBCODE_FORCE_FAIL'], $_SERVER['BBCODE_FORCE_FAIL']);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function (): void {
|
afterEach(function (): void {
|
||||||
\Mockery::close();
|
\Mockery::close();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user