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: '55'
MIN_COVERAGE: '66'
steps:
- uses: actions/checkout@v4

Expand Down
14 changes: 7 additions & 7 deletions modules/backup/controllers/IndexController.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,19 @@ public function uploadAction()
$response = $this->getResponse();
$response->setHeader('Content-Type', 'application/json', true);

if (($this->getParam('confirm', '')) !== 'RESTORE') { $response->setBody($this->_json(0, 'backup.restore.confirm')); return; }
if (($this->getParam('confirm', '')) !== 'RESTORE') { $response->setBody($this->_jsonBody(0, 'backup.restore.confirm')); return; }
$file = $_FILES['archive'] ?? null;
if (!$file || ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { $response->setBody($this->_json(0, 'backup.upload.failed')); return; }
if (!$file || ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { $response->setBody($this->_jsonBody(0, 'backup.upload.failed')); return; }

$dir = APPLICATION_ROOT . '/var/backup';
@is_dir($dir) || @mkdir($dir, 0775, true);
$tmp = $dir . '/upload-' . bin2hex(random_bytes(4)) . '.zip';
if (!@move_uploaded_file($file['tmp_name'], $tmp)) { $response->setBody($this->_json(0, 'backup.upload.failed')); return; }
if (!@move_uploaded_file($file['tmp_name'], $tmp)) { $response->setBody($this->_jsonBody(0, 'backup.upload.failed')); return; }

// Validate it's a TigerBackup archive before doing anything destructive.
if (Tiger_Backup_Archive::read($tmp, 'manifest.json') === false) {
@unlink($tmp);
$response->setBody($this->_json(0, 'backup.upload.invalid'));
$response->setBody($this->_jsonBody(0, 'backup.upload.invalid'));
return;
}

Expand All @@ -103,9 +103,9 @@ public function uploadAction()
@unlink($tmp);

if (($res['status'] ?? '') === 'ok') {
$response->setBody($this->_json(1, 'backup.restore.done', ['restored' => $res['restored']]));
$response->setBody($this->_jsonBody(1, 'backup.restore.done', ['restored' => $res['restored']]));
} else {
$response->setBody($this->_json(0, APPLICATION_ENV !== 'production' ? ('Restore failed: ' . ($res['error'] ?? '')) : 'backup.restore.failed'));
$response->setBody($this->_jsonBody(0, APPLICATION_ENV !== 'production' ? ('Restore failed: ' . ($res['error'] ?? '')) : 'backup.restore.failed'));
}
}

Expand Down Expand Up @@ -133,7 +133,7 @@ protected function _settings(): array
}

/** Minimal JSON envelope for the two non-service actions (message key is translated client-side-agnostic). */
protected function _json(int $result, string $messageKey, array $data = []): string
protected function _jsonBody(int $result, string $messageKey, array $data = []): string
{
$t = Zend_Registry::isRegistered('Zend_Translate') ? Zend_Registry::get('Zend_Translate') : null;
$msg = $t ? $t->translate($messageKey) : $messageKey;
Expand Down
18 changes: 18 additions & 0 deletions tests/COVERAGE-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,24 @@ provider/authority/GA/reCAPTCHA live HTTP, ClamAV/Rekognition scanners, AWS-SDK
mints `MODULES_PATH` etc. → the app-boot test runs `#[RunInSeparateProcess]`. **RECOMMENDED base hygiene (deferred):** reset
`Org::$_siteOrgId` in `IntegrationTestCase::tearDown`. CI floor `MIN_COVERAGE` 35 → 55.

### Wave 6 — remaining modules + the dispatch harness (LANDED 2026-07-24) → coverage 59.2% → 70.0%
First landed a **controller dispatch harness** (`tests/Support/ControllerTestCase.php`, PR #66): instantiates a
controller + dispatches ONE action with view-rendering OFF, so the action BODY runs (branch logic, `_json` body,
redirect/`_forward`) without the theme/view-script stack — unlocking `core/controllers` (0% before). Then 5 agents:
core-controllers 0→**87%** (all 6: Api 80, Auth 79, Error 95, Index 87, Page 98, Admin 91), cms 33→**94**, code 41→**90**,
agent module 6→**66-78**, system 37→**59**, + a module-controller sweep (profile 68, access 84, media/analytics/identity/
blog/signup/search/schedule controllers 90-100%). ~167 tests; combined suite **1617 green**.
- **REAL BUG FIXED (a test found it):** `Backup_IndexController::_json(int,string,array):string` was an INCOMPATIBLE
override of `Tiger_Controller_Action::_json($data,$status)` → a PHP 8.5 **fatal at class load**, so EVERY `/backup`
request 500'd (the backup admin UI was entirely broken). Renamed the helper to `_jsonBody()`; replaced the
characterization test with real dispatch coverage.
- **Harness hardening (3 agents hit it):** folded `redirector->setExit(false)` into `ControllerTestCase` setUp (the
redirector `exit`s after headers by default → would kill the PHPUnit process on a controller redirect).
- CI floor 55 → 66. Ceilings (bounded honestly, not chased): `Updates::_applyOne` would run a real `composer update`/
`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).

### 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_*`
library it builds on is now ~72% so the seams exist), `cms` module (510 @ 33%), `system` remainder (579 @ 37%), `code`
Expand Down
104 changes: 104 additions & 0 deletions tests/Integration/Access/ControllersTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.

namespace Tiger\Tests\Integration\Access;

use Access_OrgController;
use Access_UserController;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use Tiger\Tests\Support\ModuleControllerTestCase;
use Tiger_Model_Org;
use Tiger_Model_User;

/**
* The two Access admin controllers — Users and Organizations. Each is thin: index renders the
* DataTables shell (useDataTables flag), edit renders the identity/org form (create when no id,
* prefilled when an id resolves to a row). The harness dispatches each action rendering-off and asserts
* the view model — covering both the new-record and the edit-prefill branch.
*/
#[CoversClass(Access_UserController::class)]
#[CoversClass(Access_OrgController::class)]
final class ControllersTest extends ModuleControllerTestCase
{
#[Test]
public function user_index_renders_the_datatables_shell(): void
{
$this->loginAs('admin');
$this->dispatchAction(Access_UserController::class, 'index', [], 'GET');

$view = $this->controller()->view;
$this->assertStringContainsString('Users', (string) $view->title);
$this->assertTrue((bool) $view->useDataTables);
}

#[Test]
public function user_edit_renders_a_blank_form_for_a_new_user(): void
{
$this->loginAs('admin');
$this->dispatchAction(Access_UserController::class, 'edit', [], 'GET');

$view = $this->controller()->view;
$this->assertStringContainsString('New User', (string) $view->title);
$this->assertInstanceOf(\Access_Form_User::class, $view->form);
$this->assertNull($view->user);
}

#[Test]
public function user_edit_prefills_the_form_for_an_existing_user(): void
{
$id = (new Tiger_Model_User())->insert([
'email' => 'edituser@example.test',
'username' => 'edituser',
'status' => 'active',
]);
$this->loginAs('admin');
$this->dispatchAction(Access_UserController::class, 'edit', ['id' => $id], 'GET');

$view = $this->controller()->view;
$this->assertStringContainsString('Edit User', (string) $view->title);
$this->assertNotNull($view->user);
$this->assertSame('edituser@example.test', $view->form->getValues()['email']);
}

#[Test]
public function org_index_renders_the_datatables_shell(): void
{
$this->loginAs('admin');
$this->dispatchAction(Access_OrgController::class, 'index', [], 'GET');

$view = $this->controller()->view;
$this->assertStringContainsString('Organizations', (string) $view->title);
$this->assertTrue((bool) $view->useDataTables);
}

#[Test]
public function org_edit_renders_a_blank_form_for_a_new_org(): void
{
$this->loginAs('admin');
$this->dispatchAction(Access_OrgController::class, 'edit', [], 'GET');

$view = $this->controller()->view;
$this->assertStringContainsString('New Organization', (string) $view->title);
$this->assertInstanceOf(\Access_Form_Org::class, $view->form);
$this->assertNull($view->org);
}

#[Test]
public function org_edit_prefills_the_form_for_an_existing_org(): void
{
$id = (new Tiger_Model_Org())->insert([
'name' => 'Edit Org',
'slug' => 'edit-org-' . substr(md5((string) mt_rand()), 0, 8),
'status' => 'active',
]);
$this->loginAs('admin');
$this->dispatchAction(Access_OrgController::class, 'edit', ['id' => $id], 'GET');

$view = $this->controller()->view;
$this->assertStringContainsString('Edit Organization', (string) $view->title);
$this->assertNotNull($view->org);
$this->assertSame('Edit Org', $view->form->getValues()['name']);
}
}
93 changes: 93 additions & 0 deletions tests/Integration/Agent/AdminControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.

namespace Tiger\Tests\Integration\Agent;

use Agent_AdminController;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use Tiger\Tests\Support\ControllerTestCase;
use Tiger_Agent_Provider_Factory;
use Zend_Config;
use Zend_Registry;
use Zend_Session;
use Zend_Translate;

/**
* Agent_AdminController — the TigerAgent settings screen shell (Wave 6).
*
* A thin admin controller: it prefills the settings form from live config and hands the view the
* provider roster + the connected/enabled/crypto-ready flags; the actual save is the `/api` call to
* Agent_Service_Settings (covered by SettingsServiceTest). We dispatch `indexAction` with rendering
* off (ControllerTestCase) and assert the view vars the `.phtml` reads — the branch/assignment logic,
* not the markup.
*/
#[CoversClass(Agent_AdminController::class)]
final class AdminControllerTest extends ControllerTestCase
{
private bool $priorUnitTestMode;

protected function setUp(): void
{
parent::setUp();
// The admin base controller resolves the FlashMessenger helper (a session); run in ZF session
// unit-test mode so no real session is started in CLI.
$this->priorUnitTestMode = Zend_Session::$_unitTestEnabled;
Zend_Session::$_unitTestEnabled = true;
$_SESSION = [];

// The action titles the page via the registered translator; provide a minimal one.
Zend_Registry::set('Zend_Translate', new Zend_Translate([
'adapter' => 'array',
'content' => ['agent.settings.title' => 'AI Agent'],
'locale' => 'en',
]));
Zend_Registry::set('Zend_Config', new Zend_Config([], true));
}

protected function tearDown(): void
{
$_SESSION = [];
Zend_Session::$_unitTestEnabled = $this->priorUnitTestMode;
parent::tearDown();
}

#[Test]
public function index_prefills_the_form_and_exposes_the_settings_view_model(): void
{
$this->loginAs('admin');
$this->dispatchAction(Agent_AdminController::class, 'index', [], 'GET');

$view = $this->controller()->view;

$this->assertStringContainsString('AI Agent', (string) $view->title, 'the title runs through the translator');
$this->assertInstanceOf(\Agent_Form_Settings::class, $view->form, 'the settings form is handed to the view');

// The provider roster the dropdown renders is the live factory options.
$this->assertSame(Tiger_Agent_Provider_Factory::options(), $view->providers);
$this->assertArrayHasKey('anthropic', (array) $view->providers);

// Prefill + capability flags the view branches on.
$this->assertSame('anthropic', $view->provider, 'defaults to anthropic with no stored provider');
$this->assertNotSame('', (string) $view->model, 'a default model is offered');
$this->assertIsBool($view->enabled);
$this->assertIsBool($view->connected);
$this->assertIsBool($view->cryptoReady);
$this->assertContains($view->modeMax, ['ask', 'auto', 'yolo']);
}

#[Test]
public function index_reflects_stored_provider_and_model_config(): void
{
Zend_Registry::set('Zend_Config', new Zend_Config([
'tiger' => ['agent' => ['provider' => 'openai', 'model' => 'gpt-4o']],
], true));
$this->loginAs('admin');
$this->dispatchAction(Agent_AdminController::class, 'index', [], 'GET');

$view = $this->controller()->view;
$this->assertSame('openai', $view->provider, 'the stored provider is surfaced');
$this->assertSame('gpt-4o', $view->model, 'the stored model is surfaced');
}
}
Loading
Loading