policies = Mockery::mock(PolicyRepository::class); $this->versions = Mockery::mock(PolicyVersionRepository::class); $this->service = Mockery::mock(PolicyService::class); $this->controller = new PolicyController($this->policies, $this->versions, $this->service); $_POST = []; $_GET = []; Functions\when('current_user_can')->justReturn(true); Functions\when('check_admin_referer')->justReturn(true); Functions\when('wp_unslash')->returnArg(); Functions\when('sanitize_text_field')->returnArg(); Functions\when('sanitize_title')->returnArg(); Functions\when('wp_kses_post')->returnArg(); $this->stubAutop(); Functions\when('sanitize_key')->alias( static fn ($key) => strtolower((string) preg_replace('/[^a-zA-Z0-9_\-]/', '', (string) $key)) ); Functions\when('absint')->alias(static fn ($value) => abs((int) $value)); Functions\when('selected')->justReturn(''); Functions\when('wp_nonce_field')->justReturn(''); Functions\when('admin_url')->justReturn('admin.php'); Functions\when('add_query_arg')->alias( static fn (array $args, string $url) => $url . '?' . http_build_query($args) ); Functions\when('submit_button')->alias(static function (string $text = ''): void { echo $text; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- test stub }); } public function testViewingAVersionRendersItsBodyAndAnEditForm(): void { $policy = new Policy('Cancellation', 'cancellation', currentVersionId: 7, id: 4); $version = new PolicyVersion( policyId: 4, versionNumber: 2, body: '
Cancel 24 hours ahead.
', status: PolicyVersion::STATUS_PUBLISHED, publishedAt: '2026-07-01 09:00:00', id: 7, ); $_GET = [ 'policy_id' => '4', 'version_id' => '7', ]; $this->policies->shouldReceive('findAll')->andReturn([$policy]); $this->policies->shouldReceive('findById')->with(4)->andReturn($policy); $this->versions->shouldReceive('findByPolicy')->with(4)->andReturn([$version]); $this->versions->shouldReceive('findById')->with(7)->andReturn($version); $html = $this->render(); self::assertStringContainsString('Cancel 24 hours ahead.', $html); self::assertStringContainsString('value="edit_version"', $html); self::assertStringContainsString('Save as New Draft', $html); } public function testVersionListLinksToTheViewer(): void { $policy = new Policy('Cancellation', 'cancellation', currentVersionId: 7, id: 4); $version = new PolicyVersion(policyId: 4, versionNumber: 2, body: 'text', id: 7); $_GET = ['policy_id' => '4']; $this->policies->shouldReceive('findAll')->andReturn([$policy]); $this->policies->shouldReceive('findById')->with(4)->andReturn($policy); $this->versions->shouldReceive('findByPolicy')->with(4)->andReturn([$version]); $html = $this->render(); self::assertStringContainsString('page=us-policies&policy_id=4&version_id=7', $html); } public function testAVersionBelongingToAnotherPolicyIsNotShown(): void { $policy = new Policy('Cancellation', 'cancellation', id: 4); $other = new PolicyVersion(policyId: 9, versionNumber: 1, body: 'Someone else\'s policy', id: 7); $_GET = [ 'policy_id' => '4', 'version_id' => '7', ]; $this->policies->shouldReceive('findAll')->andReturn([$policy]); $this->policies->shouldReceive('findById')->with(4)->andReturn($policy); $this->versions->shouldReceive('findByPolicy')->with(4)->andReturn([]); $this->versions->shouldReceive('findById')->with(7)->andReturn($other); $html = $this->render(); self::assertStringNotContainsString('Someone else\'s policy', $html); self::assertStringNotContainsString('value="edit_version"', $html); } public function testEditingAPublishedVersionCreatesANewDraftAndLeavesTheSourceUntouched(): void { $policy = new Policy('Cancellation', 'cancellation', currentVersionId: 7, id: 4); $published = new PolicyVersion( policyId: 4, versionNumber: 2, body: 'Old text.
', status: PolicyVersion::STATUS_PUBLISHED, id: 7, ); $draft = new PolicyVersion(policyId: 4, versionNumber: 3, body: 'New text.
', id: 11); $_GET = ['policy_id' => '4']; $_POST = [ 'usc_action' => 'edit_version', 'policy_id' => '4', 'version_id' => '7', 'body' => 'New text.
', ]; $this->policies->shouldReceive('findAll')->andReturn([$policy]); $this->policies->shouldReceive('findById')->with(4)->andReturn($policy); $this->versions->shouldReceive('findByPolicy')->with(4)->andReturn([$draft, $published]); $this->versions->shouldReceive('findById')->with(7)->andReturn($published); $this->versions->shouldReceive('findById')->with(11)->andReturn($draft); // A new draft is written; the edited version itself is never updated. $this->service->shouldReceive('addDraftVersion')->once()->with(4, 'New text.
')->andReturn(11); $this->versions->shouldNotReceive('updateBody'); $html = $this->render(); // The viewer follows through to the draft that was just created. self::assertStringContainsString('New text.', $html); self::assertStringContainsString('saved as a new draft version', $html); } public function testEditingADraftVersionUpdatesItInPlace(): void { $policy = new Policy('Cancellation', 'cancellation', id: 4); $draft = new PolicyVersion( policyId: 4, versionNumber: 3, body: 'Old draft text.
', status: PolicyVersion::STATUS_DRAFT, id: 11, ); $_GET = ['policy_id' => '4']; $_POST = [ 'usc_action' => 'edit_version', 'policy_id' => '4', 'version_id' => '11', 'body' => 'Revised draft text.
', ]; $this->policies->shouldReceive('findAll')->andReturn([$policy]); $this->policies->shouldReceive('findById')->with(4)->andReturn($policy); $this->versions->shouldReceive('findByPolicy')->with(4)->andReturn([$draft]); $this->versions->shouldReceive('findById')->with(11)->andReturn($draft); // An unpublished draft is rewritten rather than branched. $this->versions->shouldReceive('updateBody')->once()->with(11, 'Revised draft text.
')->andReturn(true); $this->service->shouldNotReceive('addDraftVersion'); $html = $this->render(); self::assertStringContainsString('Draft version 3 was updated', $html); } public function testEditingAnArchivedVersionCreatesANewDraft(): void { $policy = new Policy('Cancellation', 'cancellation', currentVersionId: 9, id: 4); $archived = new PolicyVersion( policyId: 4, versionNumber: 1, body: 'Superseded text.
', status: PolicyVersion::STATUS_ARCHIVED, id: 7, ); $draft = new PolicyVersion(policyId: 4, versionNumber: 4, body: 'Reinstated text.
', id: 12); $_GET = ['policy_id' => '4']; $_POST = [ 'usc_action' => 'edit_version', 'policy_id' => '4', 'version_id' => '7', 'body' => 'Reinstated text.
', ]; $this->policies->shouldReceive('findAll')->andReturn([$policy]); $this->policies->shouldReceive('findById')->with(4)->andReturn($policy); $this->versions->shouldReceive('findByPolicy')->with(4)->andReturn([$draft, $archived]); $this->versions->shouldReceive('findById')->with(7)->andReturn($archived); $this->versions->shouldReceive('findById')->with(12)->andReturn($draft); $this->service->shouldReceive('addDraftVersion')->once()->with(4, 'Reinstated text.
')->andReturn(12); $this->versions->shouldNotReceive('updateBody'); $this->render(); } public function testEditingAVersionOfAnotherPolicyIsRejected(): void { $policy = new Policy('Cancellation', 'cancellation', id: 4); $other = new PolicyVersion(policyId: 9, versionNumber: 1, body: 'Other', id: 7); $_GET = ['policy_id' => '4']; $_POST = [ 'usc_action' => 'edit_version', 'policy_id' => '4', 'version_id' => '7', 'body' => 'Injected text', ]; $this->policies->shouldReceive('findAll')->andReturn([$policy]); $this->policies->shouldReceive('findById')->with(4)->andReturn($policy); $this->versions->shouldReceive('findByPolicy')->with(4)->andReturn([]); $this->versions->shouldReceive('findById')->with(7)->andReturn($other); $this->service->shouldNotReceive('addDraftVersion'); $this->versions->shouldNotReceive('updateBody'); $this->render(); } private function render(): string { ob_start(); $this->controller->renderPage(); return (string) ob_get_clean(); } }