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([])); } }