mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Update migrations
This commit is contained in:
@@ -78,10 +78,12 @@ class NzbCreationReliabilityTest extends TestCase
|
||||
|
||||
$this->createSchema();
|
||||
$this->seedSettings();
|
||||
NzbCreationCandidateQuery::flushCapabilityCache();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
NzbCreationCandidateQuery::flushCapabilityCache();
|
||||
$this->deleteDirectory($this->tempNzbPath);
|
||||
if ($this->databasePath !== '' && file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
@@ -143,6 +145,8 @@ class NzbCreationReliabilityTest extends TestCase
|
||||
$this->assertSame(1, DB::table('releases')->count());
|
||||
$this->assertSame(1, (int) DB::table('release_nzb_creation_failures')->where('releases_id', 1)->value('attempts'));
|
||||
$this->assertSame('Temporary filesystem failure.', DB::table('release_nzb_creation_failures')->where('releases_id', 1)->value('last_error'));
|
||||
$this->assertNotNull(DB::table('release_nzb_creation_failures')->where('releases_id', 1)->value('created_at'));
|
||||
$this->assertNotNull(DB::table('release_nzb_creation_failures')->where('releases_id', 1)->value('updated_at'));
|
||||
$this->assertNull(DB::table('releases')->where('id', 1)->value('nzb_creation_claimed_at'));
|
||||
$this->assertSame(1, DB::table('collections')->count());
|
||||
$this->assertSame('NZB creation failed; release will be retried', $nzbCreationLogger->warnings[0]['message']);
|
||||
@@ -185,6 +189,56 @@ class NzbCreationReliabilityTest extends TestCase
|
||||
$this->assertSame(3, $nzbCreationLogger->warnings[0]['context']['max_attempts']);
|
||||
}
|
||||
|
||||
public function test_repeated_transient_failure_increments_attempts_and_touches_updated_at(): void
|
||||
{
|
||||
Log::partialMock();
|
||||
Log::shouldReceive('channel')->once()->with('nzb_creation')->andReturn(new RecordingNzbCreationLogger);
|
||||
|
||||
$this->insertRelease(1, 'a');
|
||||
$this->insertCbp(200, 2000, 1);
|
||||
DB::table('release_nzb_creation_failures')->insert([
|
||||
'releases_id' => 1,
|
||||
'attempts' => 1,
|
||||
'last_error' => 'Earlier failure.',
|
||||
'created_at' => '2026-01-01 00:00:00',
|
||||
'updated_at' => '2026-01-01 00:00:00',
|
||||
]);
|
||||
|
||||
$service = $this->releaseProcessingService(
|
||||
NzbCreationResult::transient('Another filesystem failure.', [200])
|
||||
);
|
||||
|
||||
$this->assertSame(0, $service->createNZBs(null));
|
||||
|
||||
$failure = DB::table('release_nzb_creation_failures')->where('releases_id', 1)->first();
|
||||
|
||||
$this->assertSame(2, (int) $failure->attempts);
|
||||
$this->assertSame('Another filesystem failure.', $failure->last_error);
|
||||
$this->assertSame('2026-01-01 00:00:00', $failure->created_at);
|
||||
$this->assertNotSame('2026-01-01 00:00:00', $failure->updated_at);
|
||||
}
|
||||
|
||||
public function test_attempt_cap_holds_when_the_failure_relation_was_dropped(): void
|
||||
{
|
||||
Log::partialMock();
|
||||
$nzbCreationLogger = new RecordingNzbCreationLogger;
|
||||
Log::shouldReceive('channel')->once()->with('nzb_creation')->andReturn($nzbCreationLogger);
|
||||
|
||||
$this->insertRelease(1, 'a', attempts: 2);
|
||||
$this->insertCbp(200, 2000, 1);
|
||||
|
||||
$service = (new ReleaseProcessingService(
|
||||
nzb: new RelationDroppingNzbService(NzbCreationResult::transient('Repeated filesystem failure.', [200])),
|
||||
releaseManagement: new DatabaseOnlyReleaseManagementService,
|
||||
collectionCleanupService: app(CollectionCleanupService::class),
|
||||
))->setEchoCLI(false);
|
||||
|
||||
$this->assertSame(0, $service->createNZBs(null));
|
||||
$this->assertSame(0, DB::table('releases')->count());
|
||||
$this->assertSame('Deleting release after NZB creation failure', $nzbCreationLogger->warnings[0]['message']);
|
||||
$this->assertSame(3, $nzbCreationLogger->warnings[0]['context']['attempt']);
|
||||
}
|
||||
|
||||
public function test_writer_classifies_final_rename_failure_as_transient_without_final_file(): void
|
||||
{
|
||||
$this->insertRelease(1, 'd');
|
||||
@@ -458,6 +512,16 @@ class FakeNzbCreationService extends NzbService
|
||||
}
|
||||
}
|
||||
|
||||
class RelationDroppingNzbService extends FakeNzbCreationService
|
||||
{
|
||||
public function createNzbForRelease(Release $release): NzbCreationResult
|
||||
{
|
||||
$release->unsetRelation('nzbCreationFailure');
|
||||
|
||||
return parent::createNzbForRelease($release);
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseOnlyReleaseManagementService extends ReleaseManagementService
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Release;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PDO;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class ReleaseFactoryInvariantsTest extends TestCase
|
||||
{
|
||||
private string $databasePath = '';
|
||||
|
||||
public function createApplication()
|
||||
{
|
||||
$this->databasePath = sys_get_temp_dir().'/nntmux-release-factory.sqlite';
|
||||
if (file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
$pdo = new PDO('sqlite:'.$this->databasePath);
|
||||
$pdo->exec('CREATE TABLE settings (name VARCHAR PRIMARY KEY, value TEXT NULL)');
|
||||
$pdo->exec("INSERT INTO settings VALUES ('categorizeforeign', '0'), ('catwebdl', '0')");
|
||||
putenv('APP_ENV=testing');
|
||||
putenv('DB_CONNECTION=sqlite');
|
||||
putenv('DB_DATABASE='.$this->databasePath);
|
||||
$_ENV['APP_ENV'] = $_SERVER['APP_ENV'] = 'testing';
|
||||
$_ENV['DB_CONNECTION'] = $_SERVER['DB_CONNECTION'] = 'sqlite';
|
||||
$_ENV['DB_DATABASE'] = $_SERVER['DB_DATABASE'] = $this->databasePath;
|
||||
$app = require __DIR__.'/../../bootstrap/app.php';
|
||||
$app->make(Kernel::class)->bootstrap();
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
config(['database.default' => 'sqlite', 'database.connections.sqlite.database' => $this->databasePath]);
|
||||
DB::purge();
|
||||
DB::reconnect();
|
||||
DB::statement('CREATE TABLE releases (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(255) NOT NULL DEFAULT "",
|
||||
searchname VARCHAR(255) NOT NULL DEFAULT "",
|
||||
fromname VARCHAR(255) NULL,
|
||||
postdate DATETIME NULL,
|
||||
adddate DATETIME NULL,
|
||||
guid CHAR(36) NOT NULL UNIQUE,
|
||||
leftguid CHAR(1) NOT NULL,
|
||||
categories_id INTEGER NOT NULL DEFAULT 10,
|
||||
nzbstatus INTEGER NOT NULL DEFAULT 0,
|
||||
passwordstatus INTEGER NOT NULL DEFAULT -1,
|
||||
isrenamed INTEGER NOT NULL DEFAULT 0
|
||||
)');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
DB::disconnect();
|
||||
if (file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_factory_produces_uuid_guid_with_matching_leftguid(): void
|
||||
{
|
||||
for ($iteration = 0; $iteration < 5; $iteration++) {
|
||||
$attributes = Release::factory()->raw();
|
||||
|
||||
$this->assertMatchesRegularExpression(
|
||||
'/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iD',
|
||||
(string) $attributes['guid'],
|
||||
'The factory must produce a UUID guid that fits the char(36) column.',
|
||||
);
|
||||
$this->assertSame(36, mb_strlen((string) $attributes['guid']));
|
||||
$this->assertSame(
|
||||
mb_strtolower(mb_substr((string) $attributes['guid'], 0, 1)),
|
||||
mb_strtolower((string) $attributes['leftguid']),
|
||||
'leftguid must be the first character of guid.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_factory_rows_satisfy_the_release_table_constraints(): void
|
||||
{
|
||||
foreach (Release::factory()->count(3)->raw() as $attributes) {
|
||||
DB::table('releases')->insert($attributes);
|
||||
}
|
||||
|
||||
$rows = DB::table('releases')->get(['guid', 'leftguid']);
|
||||
|
||||
$this->assertCount(3, $rows);
|
||||
foreach ($rows as $row) {
|
||||
$this->assertSame(mb_substr((string) $row->guid, 0, 1), (string) $row->leftguid);
|
||||
}
|
||||
$this->assertSame(3, $rows->pluck('guid')->unique()->count());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PDO;
|
||||
use ReflectionMethod;
|
||||
use RuntimeException;
|
||||
use Tests\Integration\ReleaseSchemaOptimizationMigrationMariaDbTest;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Portable coverage for the hardening added to the releases normalization
|
||||
* migration: the chunked comment recount, the batched rollback backfill, and
|
||||
* the preflight opt-out. The full schema rebuild is MariaDB-only and lives in
|
||||
* {@see ReleaseSchemaOptimizationMigrationMariaDbTest}.
|
||||
*/
|
||||
final class ReleaseSchemaOptimizationMigrationTest extends TestCase
|
||||
{
|
||||
private string $databasePath = '';
|
||||
|
||||
public function createApplication()
|
||||
{
|
||||
$this->databasePath = sys_get_temp_dir().'/nntmux-releases-migration.sqlite';
|
||||
if (file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
$pdo = new PDO('sqlite:'.$this->databasePath);
|
||||
$pdo->exec('CREATE TABLE settings (name VARCHAR PRIMARY KEY, value TEXT NULL)');
|
||||
$pdo->exec("INSERT INTO settings VALUES ('categorizeforeign', '0'), ('catwebdl', '0')");
|
||||
$this->setEnvironmentValue('APP_ENV', 'testing');
|
||||
$this->setEnvironmentValue('DB_CONNECTION', 'sqlite');
|
||||
$this->setEnvironmentValue('DB_DATABASE', $this->databasePath);
|
||||
|
||||
$app = require __DIR__.'/../../bootstrap/app.php';
|
||||
$app->make(Kernel::class)->bootstrap();
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
config([
|
||||
'database.default' => 'sqlite',
|
||||
'database.connections.sqlite.database' => $this->databasePath,
|
||||
'nntmux.releases_optimize.chunk_size' => 100,
|
||||
'nntmux.releases_optimize.skip_preflight' => false,
|
||||
]);
|
||||
DB::purge();
|
||||
DB::reconnect();
|
||||
$this->createSchema();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
DB::disconnect();
|
||||
if (file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_comment_recount_spans_multiple_chunks_and_skips_correct_rows(): void
|
||||
{
|
||||
$releases = [];
|
||||
$comments = [];
|
||||
$commentId = 1;
|
||||
for ($id = 1; $id <= 250; $id++) {
|
||||
// Every third release already stores the right counter and must not be rewritten.
|
||||
$alreadyCorrect = $id % 3 === 0;
|
||||
$releases[] = ['id' => $id, 'guid' => 'g'.$id, 'leftguid' => 'g', 'comments' => $alreadyCorrect ? 1 : 99];
|
||||
$comments[] = ['id' => $commentId++, 'releases_id' => $id, 'isvisible' => 1];
|
||||
$comments[] = ['id' => $commentId++, 'releases_id' => $id, 'isvisible' => 0];
|
||||
}
|
||||
DB::table('releases')->insert($releases);
|
||||
DB::table('release_comments')->insert($comments);
|
||||
|
||||
$this->invoke('recountVisibleComments');
|
||||
|
||||
$this->assertSame(0, DB::table('releases')->where('comments', '<>', 1)->count());
|
||||
$this->assertSame(250, DB::table('releases')->where('comments', 1)->count());
|
||||
}
|
||||
|
||||
public function test_comment_recount_zeroes_releases_without_visible_comments(): void
|
||||
{
|
||||
DB::table('releases')->insert([
|
||||
['id' => 1, 'guid' => 'a', 'leftguid' => 'a', 'comments' => 7],
|
||||
['id' => 2, 'guid' => 'b', 'leftguid' => 'b', 'comments' => 0],
|
||||
]);
|
||||
DB::table('release_comments')->insert([['id' => 1, 'releases_id' => 1, 'isvisible' => 0]]);
|
||||
|
||||
$this->invoke('recountVisibleComments');
|
||||
|
||||
$this->assertSame(0, (int) DB::table('releases')->where('id', 1)->value('comments'));
|
||||
$this->assertSame(0, (int) DB::table('releases')->where('id', 2)->value('comments'));
|
||||
}
|
||||
|
||||
public function test_rollback_backfill_restores_sparse_rows_in_batches(): void
|
||||
{
|
||||
$releases = [];
|
||||
$passwords = [];
|
||||
$failures = [];
|
||||
for ($id = 1; $id <= 250; $id++) {
|
||||
$releases[] = ['id' => $id, 'guid' => 'g'.$id, 'leftguid' => 'g', 'comments' => 0];
|
||||
$passwords[] = ['releases_id' => $id, 'password' => 'secret-'.$id];
|
||||
$failures[] = ['releases_id' => $id, 'attempts' => $id % 5, 'last_error' => 'boom-'.$id];
|
||||
}
|
||||
DB::table('releases')->insert($releases);
|
||||
DB::table('release_nzb_passwords')->insert($passwords);
|
||||
DB::table('release_nzb_creation_failures')->insert($failures);
|
||||
|
||||
$this->invoke('restoreSparseDataToReleases');
|
||||
|
||||
$restored = DB::table('releases')->orderBy('id')->get();
|
||||
$this->assertCount(250, $restored);
|
||||
foreach ($restored as $release) {
|
||||
$this->assertSame('secret-'.$release->id, $release->nzb_password);
|
||||
$this->assertSame((int) $release->id % 5, (int) $release->nzb_creation_attempts);
|
||||
$this->assertSame('boom-'.$release->id, $release->nzb_creation_last_error);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_preflight_blocks_bad_identifiers_and_the_opt_out_bypasses_it(): void
|
||||
{
|
||||
DB::table('releases')->insert([
|
||||
['id' => 1, 'guid' => 'not-a-uuid', 'leftguid' => 'z', 'comments' => 0],
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->invoke('assertReleaseIdentifiersAreSafe');
|
||||
$this->fail('The migration accepted an invalid guid.');
|
||||
} catch (RuntimeException $e) {
|
||||
$this->assertStringContainsString('invalid release GUIDs', $e->getMessage());
|
||||
$this->assertStringContainsString('releases:normalize-guids', $e->getMessage());
|
||||
}
|
||||
|
||||
config(['nntmux.releases_optimize.skip_preflight' => true]);
|
||||
$this->invoke('assertReleaseIdentifiersAreSafe');
|
||||
$this->assertSame(1, DB::table('releases')->count());
|
||||
}
|
||||
|
||||
private function invoke(string $method): void
|
||||
{
|
||||
/** @var Migration $migration */
|
||||
$migration = require database_path('migrations/2026_08_13_001652_normalize_and_optimize_releases_table.php');
|
||||
(new ReflectionMethod($migration, $method))->invoke($migration);
|
||||
}
|
||||
|
||||
private function createSchema(): void
|
||||
{
|
||||
foreach (['release_comments', 'release_nzb_passwords', 'release_nzb_creation_failures', 'releases'] as $table) {
|
||||
DB::statement('DROP TABLE IF EXISTS '.$table);
|
||||
}
|
||||
DB::statement('CREATE TABLE releases (
|
||||
id INTEGER PRIMARY KEY, guid VARCHAR(40) NOT NULL, leftguid CHAR(1) NOT NULL,
|
||||
comments INTEGER NOT NULL DEFAULT 0, nzb_password VARCHAR(255) NULL,
|
||||
nzb_creation_attempts INTEGER NOT NULL DEFAULT 0, nzb_creation_last_error TEXT NULL
|
||||
)');
|
||||
DB::statement('CREATE TABLE release_comments (
|
||||
id INTEGER PRIMARY KEY, releases_id INTEGER NOT NULL, isvisible INTEGER NOT NULL DEFAULT 1
|
||||
)');
|
||||
DB::statement('CREATE TABLE release_nzb_passwords (
|
||||
releases_id INTEGER PRIMARY KEY, password VARCHAR(255) NOT NULL
|
||||
)');
|
||||
DB::statement('CREATE TABLE release_nzb_creation_failures (
|
||||
releases_id INTEGER PRIMARY KEY, attempts INTEGER NOT NULL DEFAULT 0, last_error TEXT NULL
|
||||
)');
|
||||
}
|
||||
|
||||
private function setEnvironmentValue(string $key, ?string $value): void
|
||||
{
|
||||
if ($value === null) {
|
||||
putenv($key);
|
||||
unset($_ENV[$key], $_SERVER[$key]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
putenv("{$key}={$value}");
|
||||
$_ENV[$key] = $_SERVER[$key] = $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PDO;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class ReleasesNormalizeGuidsTest extends TestCase
|
||||
{
|
||||
private string $databasePath = '';
|
||||
|
||||
public function createApplication()
|
||||
{
|
||||
$this->databasePath = sys_get_temp_dir().'/nntmux-normalize-guids.sqlite';
|
||||
if (file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
$pdo = new PDO('sqlite:'.$this->databasePath);
|
||||
$pdo->exec('CREATE TABLE settings (name VARCHAR PRIMARY KEY, value TEXT NULL)');
|
||||
$pdo->exec("INSERT INTO settings VALUES ('categorizeforeign', '0'), ('catwebdl', '0')");
|
||||
$this->setEnvironmentValue('APP_ENV', 'testing');
|
||||
$this->setEnvironmentValue('DB_CONNECTION', 'sqlite');
|
||||
$this->setEnvironmentValue('DB_DATABASE', $this->databasePath);
|
||||
|
||||
$app = require __DIR__.'/../../bootstrap/app.php';
|
||||
$app->make(Kernel::class)->bootstrap();
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'database.default' => 'sqlite',
|
||||
'database.connections.sqlite.database' => $this->databasePath,
|
||||
]);
|
||||
DB::purge();
|
||||
DB::reconnect();
|
||||
$this->createSchema();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
DB::disconnect();
|
||||
if (file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_leftguid_is_resynced_in_place(): void
|
||||
{
|
||||
$guid = '01234567-89ab-cdef-0123-456789abcdef';
|
||||
$this->insertRelease(1, $guid, 'f');
|
||||
|
||||
$status = Artisan::call('releases:normalize-guids');
|
||||
|
||||
$this->assertSame(0, $status);
|
||||
$this->assertSame('0', DB::table('releases')->where('id', 1)->value('leftguid'));
|
||||
$this->assertSame($guid, DB::table('releases')->where('id', 1)->value('guid'));
|
||||
}
|
||||
|
||||
public function test_dry_run_reports_without_writing(): void
|
||||
{
|
||||
$this->insertRelease(1, '01234567-89ab-cdef-0123-456789abcdef', 'f');
|
||||
$this->insertRelease(2, 'd41d8cd98f00b204e9800998ecf8427e', 'd');
|
||||
|
||||
$status = Artisan::call('releases:normalize-guids', ['--dry-run' => true]);
|
||||
$output = Artisan::output();
|
||||
|
||||
$this->assertSame(1, $status);
|
||||
$this->assertStringContainsString('Dry run: no rows are modified.', $output);
|
||||
$this->assertSame('f', DB::table('releases')->where('id', 1)->value('leftguid'));
|
||||
$this->assertSame('d41d8cd98f00b204e9800998ecf8427e', DB::table('releases')->where('id', 2)->value('guid'));
|
||||
}
|
||||
|
||||
public function test_invalid_guid_is_reported_and_never_rewritten(): void
|
||||
{
|
||||
$this->insertRelease(1, 'd41d8cd98f00b204e9800998ecf8427e', 'd', ['name' => 'Legacy nZEDb release']);
|
||||
|
||||
$status = Artisan::call('releases:normalize-guids');
|
||||
$output = Artisan::output();
|
||||
|
||||
$this->assertSame(1, $status);
|
||||
$this->assertStringContainsString('1 releases have a guid that blocks the normalization migration.', $output);
|
||||
$this->assertStringContainsString('d41d8cd98f00b204e9800998ecf8427e', $output);
|
||||
$this->assertStringContainsString('Legacy nZEDb release', $output);
|
||||
$this->assertSame('d41d8cd98f00b204e9800998ecf8427e', DB::table('releases')->where('id', 1)->value('guid'));
|
||||
}
|
||||
|
||||
public function test_case_insensitive_duplicates_report_every_id_but_the_lowest(): void
|
||||
{
|
||||
$guid = 'abcdef01-2345-6789-abcd-ef0123456789';
|
||||
$this->insertRelease(1, $guid, 'a');
|
||||
$this->insertRelease(2, strtoupper($guid), 'A');
|
||||
|
||||
$status = Artisan::call('releases:normalize-guids');
|
||||
$output = Artisan::output();
|
||||
|
||||
$this->assertSame(1, $status);
|
||||
$this->assertStringContainsString('1 releases have a guid that blocks', $output);
|
||||
$this->assertSame($guid, DB::table('releases')->where('id', 1)->value('guid'));
|
||||
$this->assertSame(strtoupper($guid), DB::table('releases')->where('id', 2)->value('guid'));
|
||||
}
|
||||
|
||||
public function test_consistent_guids_report_success(): void
|
||||
{
|
||||
$this->insertRelease(1, '01234567-89ab-cdef-0123-456789abcdef', '0');
|
||||
|
||||
$status = Artisan::call('releases:normalize-guids');
|
||||
|
||||
$this->assertSame(0, $status);
|
||||
$this->assertStringContainsString('Release guids are consistent.', Artisan::output());
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $overrides */
|
||||
private function insertRelease(int $id, string $guid, string $leftguid, array $overrides = []): void
|
||||
{
|
||||
DB::table('releases')->insert([
|
||||
'id' => $id,
|
||||
'guid' => $guid,
|
||||
'leftguid' => $leftguid,
|
||||
'name' => 'Release '.$id,
|
||||
...$overrides,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createSchema(): void
|
||||
{
|
||||
DB::statement('DROP TABLE IF EXISTS releases');
|
||||
DB::statement('CREATE TABLE releases (
|
||||
id INTEGER PRIMARY KEY,
|
||||
guid VARCHAR(40) NOT NULL,
|
||||
leftguid CHAR(1) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL DEFAULT ""
|
||||
)');
|
||||
}
|
||||
|
||||
private function setEnvironmentValue(string $key, ?string $value): void
|
||||
{
|
||||
if ($value === null) {
|
||||
putenv($key);
|
||||
unset($_ENV[$key], $_SERVER[$key]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
putenv("{$key}={$value}");
|
||||
$_ENV[$key] = $_SERVER[$key] = $value;
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,22 @@ final class ReleasesOptimizePreflightTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function test_human_report_includes_storage_discarded_data_and_index_sections(): void
|
||||
{
|
||||
$this->insertRelease(1, '01234567-89ab-cdef-0123-456789abcdef', '0', ['source' => 2]);
|
||||
|
||||
$status = Artisan::call('releases:optimize-preflight');
|
||||
$output = Artisan::output();
|
||||
|
||||
$this->assertSame(0, $status);
|
||||
$this->assertStringContainsString('Storage: not reported for the sqlite driver.', $output);
|
||||
$this->assertStringContainsString('Dropped column', $output);
|
||||
$this->assertStringContainsString('releases.source', $output);
|
||||
$this->assertStringContainsString('permanently destroyed', $output);
|
||||
$this->assertStringContainsString('Index change', $output);
|
||||
$this->assertStringContainsString('ux_releases_guid', $output);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $overrides */
|
||||
private function insertRelease(int $id, string $guid, string $leftguid, array $overrides = []): void
|
||||
{
|
||||
|
||||
@@ -113,6 +113,37 @@ final class ReleaseSchemaOptimizationMigrationMariaDbTest extends TestCase
|
||||
$this->assertDatabaseHas('release_files', ['releases_id' => 1, 'name' => 'kept.nzb']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function claim_token_and_pp_index_follow_up_narrows_columns_and_adds_size(): void
|
||||
{
|
||||
$this->seedLegacyData();
|
||||
$this->migration()->up();
|
||||
$followUp = $this->followUpMigration();
|
||||
|
||||
$followUp->up();
|
||||
|
||||
$this->assertSame(
|
||||
['passwordstatus', 'haspreview', 'nzbstatus', 'leftguid', 'postdate', 'id', 'additional_pp_claimed_at', 'size'],
|
||||
$this->indexColumns('ix_releases_add_pp_claim_queue'),
|
||||
);
|
||||
foreach (['nzb_creation_claim_token', 'additional_pp_claim_token'] as $column) {
|
||||
$this->assertSame('char(32)', strtolower($this->columnType($column)));
|
||||
$this->assertSame('ascii_general_ci', $this->columnCollation($column));
|
||||
}
|
||||
|
||||
$token = bin2hex(random_bytes(16));
|
||||
DB::table('releases')->where('id', 1)->update(['additional_pp_claim_token' => $token]);
|
||||
$this->assertSame($token, DB::table('releases')->where('id', 1)->value('additional_pp_claim_token'));
|
||||
|
||||
$followUp->down();
|
||||
|
||||
$this->assertSame(
|
||||
['passwordstatus', 'haspreview', 'nzbstatus', 'leftguid', 'postdate', 'id', 'additional_pp_claimed_at'],
|
||||
$this->indexColumns('ix_releases_add_pp_claim_queue'),
|
||||
);
|
||||
$this->assertSame('varchar(64)', strtolower($this->columnType('additional_pp_claim_token')));
|
||||
}
|
||||
|
||||
private function createSchema(): void
|
||||
{
|
||||
$releases = $this->table('releases');
|
||||
@@ -260,6 +291,14 @@ final class ReleaseSchemaOptimizationMigrationMariaDbTest extends TestCase
|
||||
return $migration;
|
||||
}
|
||||
|
||||
private function followUpMigration(): Migration
|
||||
{
|
||||
/** @var Migration $migration */
|
||||
$migration = require database_path('migrations/2026_08_14_121946_optimize_releases_claim_tokens_and_pp_index.php');
|
||||
|
||||
return $migration;
|
||||
}
|
||||
|
||||
private function table(string $name): string
|
||||
{
|
||||
return $this->tablePrefix.$name;
|
||||
|
||||
Reference in New Issue
Block a user