Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ jobs:
TIGER_TEST_DB_PASS: root
# The floor we enforce today (report-only warning above it). Bump this as coverage climbs so it
# can only go UP — the ratchet that gets us to 90% without ever regressing.
MIN_COVERAGE: '66'
MIN_COVERAGE: '72'
steps:
- uses: actions/checkout@v4

Expand Down
24 changes: 24 additions & 0 deletions tests/COVERAGE-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,30 @@ blog/signup/search/schedule controllers 90-100%). ~167 tests; combined suite **1
`vendor/` swap on a dev box (covered at guard level only); `is_uploaded_file()` uploads; live-model agent turns;
render-only `.phtml` leaves; `pcov` under-reports module `elements()` forms (multi-line-array-literal artifact).

### Wave 7 — reachable kernel + module remainders (LANDED 2026-07-25) → coverage 70.0% → 74.4%
4 parallel agents mopping up reachable-but-uncovered branches (commit-and-push-incrementally, after a
session-limit restart wiped the first attempt's exploration). 177 tests; combined suite green. Moved:
`Tiger_Backup` 47→82 · `Controller_Plugin_Authorization` 43→88 · `Cms_Renderer` 69→100 · `Location` 79→95 ·
`Code_Runtime` 82→90 · `Vendor` 80→87 · `Module_Installer` 68→74 · `Acl` 89→93 · `License_Checker` 82→91 ·
Media cluster 40→75 (Storage 100, Filesystem 97, Azure/Gcs/ClamAv/Rekognition unlocked from 0 — they were
exercised but not `#[CoversClass]`-attributed) · `Tiger_Application` 60→74 · `Vendor_Environment` 0→84 ·
`Update_Composer` 0→57 · `Google_Analytics` 29→47 · `Compat`→100 · Seo/Code/Backup module services →95-97.
Network classes covered WITHOUT network (dead-localhost curl → fail-soft, primed caches, probe subclasses,
`_run` with echo/exit). No behavioral bugs. CI floor 66 → 72.
- **Minor robustness finding (characterized, not fixed):** `Tiger_Media_Storage_Azure::put()/write()` surface a
raw `\Error` (not the friendly `RuntimeException` S3/GCS give) when the Azure SDK is absent — `_create()`
constructs the SDK class BEFORE its guarded try/catch. Small consistency fix available.

### The path from 74.4% to 80% (~800 more covered lines)
The remaining 3,673 uncovered is now dominated by genuinely-hard I/O: **kernel boot** (`Tiger_Application_Bootstrap::_init*`
+ `Application::run/boot`, ~200-400 lines — needs an IN-PROCESS APP-BOOT HARNESS, the one big reachable chunk),
`is_uploaded_file()` uploads (profile/media/agent modules ~250), composer/`vendor`-swap (system/update ~200, UNSAFE
to run), live network (google/analytics/authority ~200), ClamAv/Rekognition/AWS-SDK (~150). 80% requires the boot
harness + mopping the last scattered branches; below that is functional/live-test territory, honestly out of unit/
integration scope. Wave 8 = build the boot harness (a fixture app skeleton + `new Tiger_Application($root)` against
the test DB) → cover the `_init*` cascade.

### Next waves (priority order) — the drive to 90%
### Next waves (priority order) — the drive to 90%
### Next waves (priority order) — the drive to 90%
- **Wave 6 — the remaining MODULES:** `agent` module (462 @ 5% — needs a provider-adapter stub, but the `Tiger_Agent_*`
Expand Down
390 changes: 390 additions & 0 deletions tests/Integration/Backup/BackupCoverageTest.php

Large diffs are not rendered by default.

74 changes: 74 additions & 0 deletions tests/Integration/Backup/BackupServiceRestoreTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.

namespace Tiger\Tests\Integration\Backup;

use Backup_Service_Backup;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use Tiger\Tests\Support\IntegrationTestCase;
use Tiger_Model_Backup;

/**
* Backup_Service_Backup::restore — the EXECUTION arm BackupServiceTest left uncovered (its tests stop at
* the guards: missing confirm, missing/failed backup). Here a cataloged `ok` backup points at a
* local-disk archive that does NOT exist on disk, so `Tiger_Backup::fetchToTemp()` throws
* ("Backup file missing on disk.") — driving restore through its try/catch and the failure envelope
* WITHOUT performing a real (destructive) restore. This exercises the components-resolve → fetchToTemp →
* catch → non-'ok' → `_error` path safely.
*
* admin-gated; the fixture backup row is a DB write (rolled back per test).
*/
#[CoversClass(Backup_Service_Backup::class)]
final class BackupServiceRestoreTest extends IntegrationTestCase
{
private function dispatch(array $msg): object
{
return (new Backup_Service_Backup($msg))->getResponse();
}

private function messages(object $res): string
{
return json_encode($res->messages ?? []);
}

/** A cataloged, `ok`, local-disk backup whose archive file is absent on disk. */
private function ghostBackup(): string
{
$model = new Tiger_Model_Backup();
$id = $model->begin('TigerBackup-ghost.zip', 'local', ['database'], 'manual');
// Mark it ok with a storage_key that resolves to a non-existent local file.
$model->finish($id, 'ok', ['storage_key' => 'w7-does-not-exist/TigerBackup-ghost.zip', 'size_bytes' => 10]);
return $id;
}

#[Test]
public function restoring_a_cataloged_backup_whose_file_is_missing_fails_cleanly(): void
{
$this->loginAs('admin');
$id = $this->ghostBackup();

$res = $this->dispatch(['action' => 'restore', 'backup_id' => $id, 'confirm' => 'RESTORE']);

$this->assertSame(0, (int) $res->result, 'a missing archive fails the restore, not crashes it');
// Non-production surfaces the underlying reason; either way it never performed a restore.
$this->assertMatchesRegularExpression('/Restore failed|missing/i', $this->messages($res));

// The catalog row is untouched — restore of a broken archive is a no-op on the record.
$row = (new Tiger_Model_Backup())->findById($id);
$this->assertSame('ok', $row['outcome']);
}

#[Test]
public function restore_still_requires_the_typed_confirmation_even_for_a_valid_row(): void
{
// The guard arm alongside the execution arm: a real `ok` row but the wrong confirmation word.
$this->loginAs('admin');
$id = $this->ghostBackup();

$res = $this->dispatch(['action' => 'restore', 'backup_id' => $id, 'confirm' => 'yes']);
$this->assertSame(0, (int) $res->result, 'the destructive action needs confirm === RESTORE');
$this->assertStringContainsString('confirm', $this->messages($res));
}
}
65 changes: 65 additions & 0 deletions tests/Integration/Cms/RendererLayoutTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.

namespace Tiger\Tests\Integration\Cms;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use stdClass;
use Tiger\Tests\Support\IntegrationTestCase;
use Tiger_Cms_Renderer;
use Tiger_Model_Page;

/**
* Tiger_Cms_Renderer::render() — the LAYOUT-wrapping path (the RendererTest/RendererExtraTest units cover
* everything else). A page whose `layout_key` resolves a `type=layout` row is rendered body-first, then the
* layout body is rendered with the page's HTML handed in as `$this->content` — the CMS's page-in-layout
* composition. Needs the DB (fetchByKey), so it's an integration test; the seeded layout row rides the
* per-test transaction.
*/
#[CoversClass(Tiger_Cms_Renderer::class)]
final class RendererLayoutTest extends IntegrationTestCase
{
#[Test]
public function render_wraps_the_body_in_its_resolved_layout(): void
{
// A phtml layout that frames whatever `content` it's handed.
(new Tiger_Model_Page())->insert([
'type' => Tiger_Model_Page::TYPE_LAYOUT,
'locale' => 'en',
'org_id' => '',
'title' => 'W7 Layout',
'slug' => 'w7-layout',
'page_key' => 'w7layout',
'body' => '<main><?= $this->content ?></main>',
'format' => Tiger_Model_Page::FORMAT_PHTML,
'status' => Tiger_Model_Page::STATUS_PUBLISHED,
]);

// The page itself only needs the fields render() reads; the layout is what must be in the DB.
$page = new stdClass();
$page->body = '<p>Hello</p>';
$page->format = Tiger_Model_Page::FORMAT_HTML;
$page->layout_key = 'w7layout';
$page->locale = 'en';
$page->org_id = '';

$out = (new Tiger_Cms_Renderer())->render($page);
$this->assertSame('<main><p>Hello</p></main>', $out, 'the body is composed into the layout');
}

#[Test]
public function render_returns_the_bare_body_when_the_layout_key_resolves_nothing(): void
{
// layout_key set but no matching layout row → the render falls back to the un-wrapped body.
$page = new stdClass();
$page->body = '<p>Orphan</p>';
$page->format = Tiger_Model_Page::FORMAT_HTML;
$page->layout_key = 'w7-nonexistent-layout';
$page->locale = 'en';
$page->org_id = '';

$this->assertSame('<p>Orphan</p>', (new Tiger_Cms_Renderer())->render($page));
}
}
185 changes: 185 additions & 0 deletions tests/Integration/Code/CodeServiceConflictTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.

namespace Tiger\Tests\Integration\Code;

use Code_Service_Code;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use Tiger\Tests\Support\IntegrationTestCase;
use Tiger_Code_Modules;
use Tiger_Model_Code;
use Zend_Registry;

/**
* Code_Service_Code — the compile-conflict self-heal rails CodeServiceExtraTest left uncovered: a
* SAVE whose new active snippet redeclares a function another active snippet defines (the bundle
* `php -l` fails → the row is flagged + the last-good set stays live → "Saved, but not activated"),
* the same conflict on a local ACTIVATE, the module-snippet toggle conflict (config flag rolled back),
* plus the restore-of-a-missing-id error arm and the footer auto-insert branch.
*
* Two UNCONDITIONAL top-level `function w7dup(){}` declarations in one assembled bundle are a
* compile-time "Cannot redeclare" — exactly the cross-snippet conflict `php -l` catches (each snippet
* lints fine alone; only the union fails). superadmin-gated; bundle files are scrubbed in tearDown.
*/
#[CoversClass(Code_Service_Code::class)]
final class CodeServiceConflictTest extends IntegrationTestCase
{
private const SLUG = 'w7codemod';

private string $moduleDir;

protected function setUp(): void
{
parent::setUp();
Zend_Registry::set('tiger.auth.stateless', true);

// Two module snippets that BOTH define an unguarded top-level w7mdup() → activating both is a
// redeclare conflict (used to exercise _toggleModule's roll-back arm).
$this->moduleDir = APPLICATION_PATH . '/modules/' . self::SLUG;
$snip = $this->moduleDir . '/snippets';
@mkdir($snip, 0777, true);
file_put_contents($snip . '/dupa.php', "<?php\n// tiger:snippet label=\"Dup A\" scope=\"global\"\nfunction w7mdup() { return 'a'; }\n");
file_put_contents($snip . '/dupb.php', "<?php\n// tiger:snippet label=\"Dup B\" scope=\"global\"\nfunction w7mdup() { return 'b'; }\n");
}

protected function tearDown(): void
{
Zend_Registry::set('tiger.auth.stateless', false);
$this->rmrf($this->moduleDir);
$dir = APPLICATION_ROOT . '/storage/cache/code';
foreach (glob($dir . '/global.*.php') ?: [] as $f) { @unlink($f); }
foreach (glob($dir . '/inject.global.*.php') ?: [] as $f) { @unlink($f); }
parent::tearDown();
}

private function rmrf(string $dir): void
{
if (!is_dir($dir)) { @unlink($dir); return; }
foreach (scandir($dir) as $e) {
if ($e === '.' || $e === '..') { continue; }
$p = $dir . '/' . $e;
is_dir($p) && !is_link($p) ? $this->rmrf($p) : @unlink($p);
}
@rmdir($dir);
}

private function call(string $action, array $params = []): object
{
return (new Code_Service_Code(['action' => $action] + $params))->getResponse();
}

/** Insert an ACTIVE local PHP snippet defining an unconditional top-level function. */
private function seedActive(string $name, string $fn): string
{
return (new Tiger_Model_Code())->insert([
'org_id' => '',
'name' => $name,
'language' => Tiger_Model_Code::LANG_PHP,
'code' => "function {$fn}() { return 1; }",
'run_location' => Tiger_Model_Code::LOC_GLOBAL,
'active' => 1,
'status' => Tiger_Model_Code::STATUS_ACTIVE,
]);
}

// ----- SAVE that conflicts with the running set ---------------------------------------------

#[Test]
public function saving_a_snippet_that_redeclares_an_active_function_is_stored_but_not_activated(): void
{
$this->loginAs('superadmin');
$this->seedActive('First', 'w7dup'); // already active + in the bundle

// A second active snippet redeclaring w7dup — lints fine alone, but the assembled bundle can't.
$res = $this->call('save', [
'name' => 'Second',
'language' => Tiger_Model_Code::LANG_PHP,
'code' => 'function w7dup() { return 2; }',
'active' => '1',
'priority' => '100',
]);

$this->assertSame(0, (int) $res->result, 'the conflicting save is not fully applied');
$this->assertStringContainsString('Saved, but not activated', json_encode($res->messages));
// The conflict path errors without a data payload — but the row WAS persisted (save ran before
// the rebuild), and the self-heal flagged it off so the last-good set stays live.
$active = $this->db->fetchOne('SELECT active FROM code WHERE name = ?', ['Second']);
$this->assertNotFalse($active, 'the snippet was still persisted');
$this->assertSame(0, (int) $active, 'deactivated by the self-heal');
}

// ----- ACTIVATE that conflicts with the running set -----------------------------------------

#[Test]
public function activating_a_local_snippet_that_conflicts_is_refused_and_self_heals(): void
{
$this->loginAs('superadmin');
$this->seedActive('Alpha', 'w7dup');

// Beta redeclares w7dup but starts inactive; turning it on makes the union fail to compile.
$beta = (new Tiger_Model_Code())->insert([
'org_id' => '', 'name' => 'Beta', 'language' => Tiger_Model_Code::LANG_PHP,
'code' => 'function w7dup() { return 9; }', 'run_location' => Tiger_Model_Code::LOC_GLOBAL,
'active' => 0, 'status' => Tiger_Model_Code::STATUS_DRAFT,
]);

$res = $this->call('activate', ['code_id' => $beta]);
$this->assertSame(0, (int) $res->result, 'a conflicting activate is refused');
$this->assertStringContainsString('conflicts with the running set', json_encode($res->messages));
$this->assertSame(0, (int) $this->db->fetchOne('SELECT active FROM code WHERE code_id = ?', [$beta]), 'left inactive by the self-heal');
}

// ----- MODULE-snippet toggle that conflicts (config flag rolled back) ------------------------

#[Test]
public function activating_a_conflicting_module_snippet_rolls_back_the_config_flag(): void
{
$this->loginAs('superadmin');
$keyA = self::SLUG . '/dupa';
$keyB = self::SLUG . '/dupb';

$onA = $this->call('activate', ['code_id' => 'module:' . $keyA]);
$this->assertSame(1, (int) $onA->result, 'the first module snippet activates cleanly');
$this->assertTrue(Tiger_Code_Modules::isActive($keyA));

// The second redeclares w7mdup → the union won't compile → the flag is rolled back off.
$onB = $this->call('activate', ['code_id' => 'module:' . $keyB]);
$this->assertSame(0, (int) $onB->result, 'the conflicting module snippet is refused');
$this->assertStringContainsString('conflicts with the running set', json_encode($onB->messages));
$this->assertFalse(Tiger_Code_Modules::isActive($keyB), 'its active-set flag was rolled back');
$this->assertTrue(Tiger_Code_Modules::isActive($keyA), 'the first snippet stays active');
}

// ----- restore of a missing id (the catch arm) ----------------------------------------------

#[Test]
public function restoring_a_missing_snippet_is_a_clean_error(): void
{
$this->loginAs('superadmin');
// A well-formed request (version >= 1) whose id has no versions → restoreVersion throws → caught.
$res = $this->call('restore', ['code_id' => 'no-such-code-id', 'version' => 2]);
$this->assertSame(0, (int) $res->result, 'a missing snippet restore fails cleanly, no crash');
}

// ----- footer auto-insert branch ------------------------------------------------------------

#[Test]
public function saving_a_js_snippet_with_footer_auto_insert_stores_the_footer_location(): void
{
$this->loginAs('superadmin');
$res = $this->call('save', [
'name' => 'Footer JS',
'language' => Tiger_Model_Code::LANG_JS,
'code' => 'console.log("hi");',
'auto_insert' => Tiger_Model_Code::AUTO_FOOTER,
'active' => '1',
'priority' => '20',
]);
$this->assertSame(1, (int) $res->result, 'a client-JS snippet needs no PHP parse-check');
$row = (new Tiger_Model_Code())->findById($res->data['code_id']);
$this->assertSame(Tiger_Model_Code::LANG_JS, $row->language);
$this->assertSame(Tiger_Model_Code::AUTO_FOOTER, $row->auto_insert, 'js honors an explicit footer placement');
}
}
Loading
Loading