CI / Tests (PHP 8.2) (pull_request) Successful in 51s
CI / No Debug Code (pull_request) Successful in 2s
CI / Coding Standards (pull_request) Successful in 2m54s
CI / PHPStan (pull_request) Successful in 2m55s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m43s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.1) (pull_request) Successful in 42s
`true` as a return type is PHP 8.2, but the plugin advertises 8.1, so the family screen's two service calls fataled on the 8.1 test job while every other job passed. They now return `?\WP_Error` — null on success — which matches RegistrationGate::validate() and works on 8.1. PHPStan was analysing against whatever PHP happened to be running (8.3 in CI, newer locally), so `composer lint` was green on syntax the plugin promises not to use. It is now pinned to the supported 8.1-8.3 range, which reproduces this failure at lint time instead of three jobs later. Co-Authored-By: Claude Opus 5 <[email protected]>
277 lines
9.5 KiB
PHP
277 lines
9.5 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Unsupervised\Schedular\Tests\Unit\Guardian;
|
|
|
|
use Brain\Monkey\Functions;
|
|
use Mockery;
|
|
use Unsupervised\Schedular\Guardian\FamilyPage;
|
|
use Unsupervised\Schedular\Guardian\GuardianService;
|
|
use Unsupervised\Schedular\Registration\Answer;
|
|
use Unsupervised\Schedular\Registration\AnswerRepository;
|
|
use Unsupervised\Schedular\Registration\Question;
|
|
use Unsupervised\Schedular\Registration\QuestionRepository;
|
|
use Unsupervised\Schedular\Tests\Unit\TestCase;
|
|
|
|
class FamilyPageTest extends TestCase
|
|
{
|
|
private GuardianService&Mockery\MockInterface $guardians;
|
|
private QuestionRepository&Mockery\MockInterface $questions;
|
|
private AnswerRepository&Mockery\MockInterface $answers;
|
|
private FamilyPage $page;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->guardians = Mockery::mock(GuardianService::class);
|
|
$this->questions = Mockery::mock(QuestionRepository::class);
|
|
$this->answers = Mockery::mock(AnswerRepository::class);
|
|
|
|
$this->page = new FamilyPage($this->guardians, $this->questions, $this->answers);
|
|
|
|
$_POST = [];
|
|
$_GET = [];
|
|
|
|
Functions\when('is_user_logged_in')->justReturn(true);
|
|
Functions\when('get_current_user_id')->justReturn(5);
|
|
Functions\when('wp_enqueue_style')->justReturn(null);
|
|
Functions\when('wp_nonce_field')->justReturn('');
|
|
Functions\when('get_permalink')->justReturn('https://studio.test/family/');
|
|
Functions\when('absint')->alias(static fn ($value) => abs((int) $value));
|
|
Functions\when('sanitize_key')->alias(static fn (string $v): string => strtolower(preg_replace('/[^a-z0-9_\-]/i', '', $v) ?? ''));
|
|
Functions\when('sanitize_text_field')->alias(static fn (string $v): string => trim($v));
|
|
Functions\when('sanitize_textarea_field')->alias(static fn (string $v): string => trim($v));
|
|
Functions\when('wp_unslash')->alias(static fn ($v) => $v);
|
|
Functions\when('check_admin_referer')->justReturn(true);
|
|
Functions\when('add_query_arg')->alias(
|
|
static fn (string $key, $value, string $url): string => $url . '?' . $key . '=' . $value
|
|
);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
$_POST = [];
|
|
$_GET = [];
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* A FamilyPage whose redirect is captured instead of exiting the process.
|
|
*
|
|
* @param-out string $captured
|
|
*/
|
|
private function capturingPage(?string &$captured): FamilyPage
|
|
{
|
|
$page = Mockery::mock(FamilyPage::class, [$this->guardians, $this->questions, $this->answers])
|
|
->makePartial()
|
|
->shouldAllowMockingProtectedMethods();
|
|
|
|
$page->shouldReceive('redirect')->andReturnUsing(static function (string $url) use (&$captured): void {
|
|
$captured = $url;
|
|
});
|
|
|
|
return $page;
|
|
}
|
|
|
|
private function question(int $id, bool $required): Question
|
|
{
|
|
return new Question(offeringId: null, label: 'Instrument', isRequired: $required, scope: Question::SCOPE_ACCOUNT, id: $id);
|
|
}
|
|
|
|
public function testLoggedOutVisitorIsOfferedALoginLink(): void
|
|
{
|
|
Functions\when('is_user_logged_in')->justReturn(false);
|
|
Functions\when('wp_login_url')->justReturn('https://studio.test/wp-login.php');
|
|
|
|
$html = $this->page->render([]);
|
|
|
|
self::assertStringContainsString('log in to manage your family', $html);
|
|
}
|
|
|
|
public function testRenderListsTheGuardiansChildren(): void
|
|
{
|
|
$this->guardians->shouldReceive('children')->once()->with(5)->andReturn([
|
|
['id' => 42, 'name' => 'Ada', 'date_of_birth' => '2015-04-02', 'relationship' => 'Parent'],
|
|
]);
|
|
$this->questions->shouldReceive('findByScope')->andReturn([]);
|
|
|
|
$html = $this->page->render([]);
|
|
|
|
self::assertStringContainsString('Ada', $html);
|
|
self::assertStringContainsString('2015-04-02', $html);
|
|
self::assertStringContainsString('Add a child', $html);
|
|
}
|
|
|
|
public function testAddCreatesTheChildRecordsItsAnswersAndRedirects(): void
|
|
{
|
|
$_POST = [
|
|
'us_family_action' => 'add',
|
|
'child_name' => 'Ada',
|
|
'child_dob' => '2015-04-02',
|
|
'child_relationship' => 'Parent',
|
|
'us_answers' => [7 => 'Piano'],
|
|
];
|
|
|
|
$this->questions->shouldReceive('findByScope')->once()->andReturn([$this->question(7, true)]);
|
|
$this->guardians->shouldReceive('createChild')->once()->with(5, 'Ada', '2015-04-02', 'Parent')->andReturn(42);
|
|
|
|
$this->answers->shouldReceive('insert')
|
|
->once()
|
|
->with(Mockery::on(static fn (Answer $a): bool =>
|
|
$a->questionId === 7
|
|
&& $a->studentId === 42
|
|
&& $a->registrationId === 42
|
|
&& $a->registrationType === Answer::REG_ACCOUNT
|
|
&& $a->answerValue === 'Piano'));
|
|
|
|
$captured = null;
|
|
$this->capturingPage($captured)->maybeHandleSubmit();
|
|
|
|
self::assertSame('https://studio.test/family/?us_family=added', $captured);
|
|
}
|
|
|
|
/**
|
|
* Validating before creating is what stops a missing answer from leaving a
|
|
* half-added child behind.
|
|
*/
|
|
public function testAddRefusesAMissingRequiredAnswerBeforeCreatingTheChild(): void
|
|
{
|
|
$_POST = [
|
|
'us_family_action' => 'add',
|
|
'child_name' => 'Ada',
|
|
'us_answers' => [7 => ' '],
|
|
];
|
|
|
|
$this->questions->shouldReceive('findByScope')->once()->andReturn([$this->question(7, true)]);
|
|
$this->guardians->shouldNotReceive('createChild');
|
|
$this->answers->shouldNotReceive('insert');
|
|
|
|
$captured = null;
|
|
$page = $this->capturingPage($captured);
|
|
$page->shouldNotReceive('redirect');
|
|
|
|
$page->maybeHandleSubmit();
|
|
|
|
self::assertNull($captured);
|
|
}
|
|
|
|
public function testAddSurfacesAServiceErrorInsteadOfRedirecting(): void
|
|
{
|
|
$_POST = ['us_family_action' => 'add', 'child_name' => ''];
|
|
|
|
$this->questions->shouldReceive('findByScope')->once()->andReturn([]);
|
|
$this->guardians->shouldReceive('createChild')->once()->andReturn(new \WP_Error('missing_name', 'Please give each child a name.'));
|
|
$this->answers->shouldNotReceive('insert');
|
|
|
|
$captured = null;
|
|
$page = $this->capturingPage($captured);
|
|
$page->shouldNotReceive('redirect');
|
|
|
|
$page->maybeHandleSubmit();
|
|
|
|
self::assertNull($captured);
|
|
}
|
|
|
|
public function testEditDelegatesToTheServiceAndRedirects(): void
|
|
{
|
|
$_POST = [
|
|
'us_family_action' => 'edit',
|
|
'child_id' => '42',
|
|
'child_name' => 'Ada L',
|
|
'child_dob' => '2015-04-02',
|
|
];
|
|
|
|
$this->guardians->shouldReceive('updateChild')->once()->with(5, 42, 'Ada L', '2015-04-02')->andReturn(null);
|
|
|
|
$captured = null;
|
|
$this->capturingPage($captured)->maybeHandleSubmit();
|
|
|
|
self::assertSame('https://studio.test/family/?us_family=updated', $captured);
|
|
}
|
|
|
|
public function testRemoveDelegatesToTheServiceAndRedirects(): void
|
|
{
|
|
$_POST = ['us_family_action' => 'remove', 'child_id' => '42'];
|
|
|
|
$this->guardians->shouldReceive('removeChild')->once()->with(5, 42)->andReturn(null);
|
|
|
|
$captured = null;
|
|
$this->capturingPage($captured)->maybeHandleSubmit();
|
|
|
|
self::assertSame('https://studio.test/family/?us_family=removed', $captured);
|
|
}
|
|
|
|
public function testRemoveRefusalIsShownRatherThanRedirected(): void
|
|
{
|
|
$_POST = ['us_family_action' => 'remove', 'child_id' => '42'];
|
|
|
|
$this->guardians->shouldReceive('removeChild')->once()->andReturn(
|
|
new \WP_Error('has_history', 'This child has lessons or enrolments on record.')
|
|
);
|
|
|
|
$captured = null;
|
|
$page = $this->capturingPage($captured);
|
|
$page->shouldNotReceive('redirect');
|
|
|
|
$page->maybeHandleSubmit();
|
|
|
|
self::assertNull($captured);
|
|
}
|
|
|
|
public function testAnUnrecognisedActionDoesNothing(): void
|
|
{
|
|
$_POST = ['us_family_action' => 'destroy'];
|
|
|
|
$this->guardians->shouldNotReceive('createChild');
|
|
$this->guardians->shouldNotReceive('removeChild');
|
|
|
|
$captured = null;
|
|
$page = $this->capturingPage($captured);
|
|
$page->shouldNotReceive('redirect');
|
|
|
|
$page->maybeHandleSubmit();
|
|
|
|
self::assertNull($captured);
|
|
}
|
|
|
|
public function testNoActionIsANoOp(): void
|
|
{
|
|
$this->guardians->shouldNotReceive('createChild');
|
|
|
|
$captured = null;
|
|
$page = $this->capturingPage($captured);
|
|
$page->shouldNotReceive('redirect');
|
|
|
|
$page->maybeHandleSubmit();
|
|
|
|
self::assertNull($captured);
|
|
}
|
|
|
|
public function testLoggedOutSubmissionIsIgnored(): void
|
|
{
|
|
Functions\when('is_user_logged_in')->justReturn(false);
|
|
$_POST = ['us_family_action' => 'remove', 'child_id' => '42'];
|
|
|
|
$this->guardians->shouldNotReceive('removeChild');
|
|
|
|
$captured = null;
|
|
$page = $this->capturingPage($captured);
|
|
$page->shouldNotReceive('redirect');
|
|
|
|
$page->maybeHandleSubmit();
|
|
|
|
self::assertNull($captured);
|
|
}
|
|
|
|
public function testCompletedActionRendersItsConfirmation(): void
|
|
{
|
|
$_GET = ['us_family' => 'added'];
|
|
|
|
$this->guardians->shouldReceive('children')->andReturn([]);
|
|
$this->questions->shouldReceive('findByScope')->andReturn([]);
|
|
|
|
self::assertStringContainsString('Child added.', $this->page->render([]));
|
|
}
|
|
}
|