From f97b8a4576cca29ddcf767c9d648540b47af3019 Mon Sep 17 00:00:00 2001 From: James Griffin Date: Thu, 30 Jul 2026 15:08:05 -0300 Subject: [PATCH] Let the account holder edit their own profile details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Profile block is headed "Your profile", but the one person on it you could not change was yourself: your name, your birth year, and whether you take lessons yourself were fixed at whatever signup recorded, and correcting any of them meant asking a studio admin. A "Your details" section now opens the page, saved through the same nonce-checked template_redirect post/redirect/get path the child rows use: - Your name, written to display_name and nickname together, for the reason updateChild() does — UserName reads the nickname first, and leaving it behind would put the account's email address back on every screen that names a person. - "I take lessons myself", the positive of us_guardian_only. This makes good on the claim already in bookableStudents() and the feature doc that a guardian-only account can put itself right from the profile page. - Your birth year, held to the same normaliseBirthYear() rule as every other student. The email is shown but not editable: it is the account's user_login as well as its address, so changing it stays a studio-side job. The birth-year field deliberately carries no `required` attribute. It is asked of a student only, and this page loads no JavaScript, so a browser-enforced `required` would leave a guardian who books solely for other people unable to submit the form at all; handleSelf() enforces it against the checkbox instead. Unticking the box does not clear a stored birth year — it says who books, not "forget what is on file". Closes #165 Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 2 + assets/css/frontend.css | 26 +++++ docs/features/parent-guardian-accounts.md | 52 ++++++++- src/BlockPreview.php | 27 ++++- src/Guardian/FamilyPage.php | 23 +++- src/Guardian/GuardianService.php | 66 ++++++++++++ templates/frontend/family-page.php | 47 ++++++++ tests/Unit/BlockPreviewTest.php | 17 +++ tests/Unit/Guardian/FamilyPageTest.php | 112 ++++++++++++++++++++ tests/Unit/Guardian/GuardianServiceTest.php | 81 ++++++++++++++ 10 files changed, 443 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88d5115..dc8c6fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ each change under the current top section as you work. ### Added - **A registration question can now be asked of students only, and can be required of a student without being required of the account holder.** Every account-signup question was asked of everybody who registered, on the same terms — so "School and grade" had to be put to the adult signing themselves up, and a question a studio needed answered for a child could only be made required by demanding it of everyone. Each question now says who it is asked of — everyone, or only the students you register on behalf of — and carries its own **Required** setting for each: optional for you, required for every student you enrol, is now a thing a studio can ask for. Existing questions are untouched: they stay asked of everyone, and one that was required stays required of everyone. +- **You can now edit your own details on the profile page**, not just your students'. The page is called **Your profile**, and until now the one person on it you could not change was yourself: a mistyped name at signup, or a name that had since changed, meant asking the studio to fix it. **Your details** now sits at the top of the page with your name, your birth year, and whether you take lessons yourself. Your email address is shown but not editable — it is also how you sign in, so changing it stays a studio-side job. +- **"I take lessons myself" can be corrected after signup.** Signup asks whether you are registering just yourself, only on behalf of students, or both, and the answer decides whether you are offered as a student when booking. Choosing wrongly — or taking up lessons later alongside the children you book for — used to leave you asking the studio to change it. Ticking the box makes you bookable again and asks for your birth year like any other student; unticking it takes you back off the list without discarding the birth year you already gave, so ticking it back on costs you nothing. ## [1.4.1] diff --git a/assets/css/frontend.css b/assets/css/frontend.css index 73da434..7a49aa6 100644 --- a/assets/css/frontend.css +++ b/assets/css/frontend.css @@ -521,6 +521,32 @@ } /* The guardian's manage-children screen ([us_family]). */ + +/* + * The account holder's own details, set off from the students below so the two + * halves of the page do not read as one long form. + */ +.us-family-self { + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid #eee; +} + +.us-family-self-email span { + font-weight: 600; +} + +/* + * Guidance under a control, not a label: it explains when a field matters + * rather than naming it, so it is sized down and reads after the input. + */ +.us-field-hint { + display: block; + margin-top: 4px; + font-size: 0.9em; + opacity: 0.75; +} + .us-family-list { margin: 0 0 20px; padding: 0; diff --git a/docs/features/parent-guardian-accounts.md b/docs/features/parent-guardian-accounts.md index 445540b..626f5bc 100644 --- a/docs/features/parent-guardian-accounts.md +++ b/docs/features/parent-guardian-accounts.md @@ -187,7 +187,8 @@ than writing `0`, so "not set" stays the one spelling of "yes, a student". One guard: a guardian-only account with **nobody linked to it** is still offered itself, because an empty picker is no way to book at all. They can put the -account right from the profile page. +account right from the profile page — see **Your details** below, where the flag +is editable as "I take lessons myself". Per child the form collects: - **Name** (required) @@ -267,9 +268,51 @@ Booking-scope policies are accepted at booking time by whoever is signed in; `BookingEndpoint` passes the same `accepted_by` when a guardian books for a child. -## Managing children +## Managing the account -`[us_family]` (block: **Profile**) renders the guardian's manage-children screen: +### Your details + +The profile screen opens with the account holder's own record, because the +alternative was a page called **Your profile** on which the one person who could +not be edited was you. The form saves through +`GuardianService::updateSelf()` and holds: + +- **Your name** — `display_name` and `nickname`, written together for the reason + `updateChild()` does: `UserName` reads the nickname first, and leaving it + behind would put the account's email address back on every screen that names a + person. +- **I take lessons myself** — the positive of `us_guardian_only`, so the form + asks the question the way a person answers it and `updateSelf()` is the one + place the sense is flipped. This is what makes good on "they can put the + account right from the profile page": an account that registered as a pure + guardian and later took up lessons — or ticked the wrong radio at signup — can + now correct itself instead of asking the studio to. +- **Your birth year** — `us_birth_year`, the same meta and the same + `normaliseBirthYear()` rule every student is held to. + +Two decisions worth keeping: + +- The birth-year field carries **no `required` attribute**. It is asked of a + student only, and the family screen loads no JavaScript, so a browser-enforced + `required` would leave a guardian who books solely for other people unable to + submit the form at all. `FamilyPage::handleSelf()` enforces it against the + checkbox instead, which is where the condition actually lives. +- Unticking **I take lessons myself** does **not** clear a stored birth year. + The box says who books, not "forget what you know about me", and someone who + ticks it back on the next visit should find their details as they left them. + +The **email** is shown but not editable: it is the account's `user_login` as +well as its address, so changing it is a studio-side job rather than a +profile-screen one. + +The account-scope questions are not re-asked here, in either direction — the +child rows do not offer them on edit either, and a studio that needs a newly +self-declared student's answers asks for them the same way it would for any +other change of circumstance. + +### Managing children + +The rest of `[us_family]` (block: **Profile**) is the manage-children screen: list the children, add one, edit a name/birth year, remove one. - **Add** creates another accountless child user and links it. Account-scope @@ -366,7 +409,8 @@ need — via `GuardianService::contactFor()`. - Models: `Unsupervised\Schedular\Guardian\GuardianLink` - Repository: `Unsupervised\Schedular\Guardian\GuardianRepository` - Service: `Unsupervised\Schedular\Guardian\GuardianService` (child creation, - `canActFor()`, `payerFor()`, `contactFor()`, removal rules) + `canActFor()`, `payerFor()`, `contactFor()`, removal rules, and the account + holder's own record via `accountHolder()`/`updateSelf()`) - Login block: `Unsupervised\Schedular\Guardian\ChildLoginGate` - Frontend: `Unsupervised\Schedular\Guardian\FamilyPage` (`[us_family]`) - Shared question field: `Unsupervised\Schedular\Registration\QuestionField` diff --git a/src/BlockPreview.php b/src/BlockPreview.php index c1c3f65..a88f185 100644 --- a/src/BlockPreview.php +++ b/src/BlockPreview.php @@ -176,10 +176,26 @@ class BlockPreview { } /** - * Sample family (manage-children) page: two representative children and the - * add form, with the controls inert so the editor preview cannot post. + * Sample family page: the account holder's own details, two representative + * children and the add form, with the controls inert so the editor preview + * cannot post. */ public static function family(): string { + $self = sprintf( + '

%s

%s grace@example.com

' + . '

' + . '

' + . '

' + . '

', + esc_html__( 'Your details', 'unsupervised-schedular' ), + esc_html__( 'Email', 'unsupervised-schedular' ), + esc_html__( 'Your name', 'unsupervised-schedular' ), + esc_attr__( 'Grace Hopper', 'unsupervised-schedular' ), + esc_html__( 'I take lessons myself', 'unsupervised-schedular' ), + esc_html__( 'Your birth year', 'unsupervised-schedular' ), + esc_html__( 'Save my details', 'unsupervised-schedular' ) + ); + $children = ''; foreach ( [ 'Ada Lovelace', 'Alan Turing' ] as $name ) { $children .= sprintf( @@ -202,9 +218,12 @@ class BlockPreview { ); return sprintf( - '
%s

%s

    %s
%s
', - self::note( __( 'Editor preview — signed-in guardians see and manage their own students here.', 'unsupervised-schedular' ) ), + '
%s

%s

%s
' + . '

%s

    %s
%s
', + self::note( __( 'Editor preview — signed-in visitors see and manage their own details and students here.', 'unsupervised-schedular' ) ), esc_html__( 'Your profile', 'unsupervised-schedular' ), + $self, + esc_html__( 'Your students', 'unsupervised-schedular' ), $children, $add ); diff --git a/src/Guardian/FamilyPage.php b/src/Guardian/FamilyPage.php index a1c1628..8b7c5f3 100644 --- a/src/Guardian/FamilyPage.php +++ b/src/Guardian/FamilyPage.php @@ -10,8 +10,8 @@ use Unsupervised\Schedular\Registration\QuestionRepository; use Unsupervised\Schedular\Val; /** - * The guardian's "my family" screen (`[us_family]`): list, add, edit and remove - * the children they book for. + * The guardian's "my family" screen (`[us_family]`): their own details, plus + * list, add, edit and remove the children they book for. * * Submissions are processed on `template_redirect` — before any output — and * post/redirect/get back to the page, so a refresh cannot resubmit and add the @@ -23,6 +23,7 @@ class FamilyPage { private const RESULT_ADDED = 'added'; private const RESULT_UPDATED = 'updated'; private const RESULT_REMOVED = 'removed'; + private const RESULT_SELF = 'self'; /** * Error from the most recent submission processed on `template_redirect`, @@ -58,6 +59,7 @@ class FamilyPage { $userId = get_current_user_id(); + $self = $this->guardians->accountHolder( $userId ); $children = $this->guardians->children( $userId ); $questions = $this->questions->findByScope( Question::SCOPE_ACCOUNT, activeOnly: true ); $error = $this->submitError; @@ -99,6 +101,7 @@ class FamilyPage { 'add' => $this->handleAdd( $userId ), 'edit' => $this->handleEdit( $userId ), 'remove' => $this->handleRemove( $userId ), + 'self' => $this->handleSelf( $userId ), default => new \WP_Error( 'unknown_action', __( 'Unrecognised request.', 'unsupervised-schedular' ) ), }; @@ -149,6 +152,21 @@ class FamilyPage { return $error ?? self::RESULT_UPDATED; } + /** + * Save the account holder's own details. The birth year is only asked of a + * student, so it is the checkbox — not the browser — that decides whether one + * is required; the field carries no `required` attribute, or a guardian who + * books only for other people could never submit the form at all. + */ + private function handleSelf( int $userId ): string|\WP_Error { + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by the caller. + $isStudent = isset( $_POST['is_student'] ); + + $error = $this->guardians->updateSelf( $userId, $this->postString( 'own_name' ), $this->postString( 'own_birth_year' ), $isStudent ); + + return $error ?? self::RESULT_SELF; + } + private function handleRemove( int $guardianId ): string|\WP_Error { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by the caller. $childId = absint( Val::int( $_POST['child_id'] ?? 0 ) ); @@ -244,6 +262,7 @@ class FamilyPage { self::RESULT_ADDED => __( 'Student added.', 'unsupervised-schedular' ), self::RESULT_UPDATED => __( 'Details updated.', 'unsupervised-schedular' ), self::RESULT_REMOVED => __( 'Student removed.', 'unsupervised-schedular' ), + self::RESULT_SELF => __( 'Your details have been updated.', 'unsupervised-schedular' ), default => '', }; } diff --git a/src/Guardian/GuardianService.php b/src/Guardian/GuardianService.php index 48b4e22..3311624 100644 --- a/src/Guardian/GuardianService.php +++ b/src/Guardian/GuardianService.php @@ -163,6 +163,52 @@ class GuardianService { return null; } + /** + * Update the account holder's own details from the profile screen: their + * name, whether they are a student in their own right, and — when they are — + * their birth year. + * + * `$isStudent` is the positive of what {@see META_GUARDIAN_ONLY} stores, so + * the form can ask the question the way a person would answer it and this is + * the single place the sense is flipped. + * + * Returns null on success, mirroring {@see updateChild()}. + */ + public function updateSelf( int $userId, string $name, string $birthYear, bool $isStudent ): ?\WP_Error { + $name = trim( $name ); + if ( '' === $name ) { + return new \WP_Error( 'missing_name', __( 'Please give your name.', 'unsupervised-schedular' ) ); + } + + if ( $isStudent && 0 === self::normaliseBirthYear( $birthYear ) ) { + return new \WP_Error( 'missing_birth_year', self::ownBirthYearError() ); + } + + $result = wp_update_user( + [ + 'ID' => $userId, + 'display_name' => $name, + 'nickname' => $name, + ] + ); + + if ( is_wp_error( $result ) ) { + return $result; + } + + $this->setGuardianOnly( $userId, ! $isStudent ); + + // Only written when they are a student. Saying "I only book for other + // people" is a statement about who books, not an instruction to forget a + // year already on file — and someone who ticks the box back on the next + // visit should find their own details as they left them. + if ( $isStudent ) { + $this->setBirthYear( $userId, $birthYear ); + } + + return null; + } + /** * Unlink a child and delete their account. Refused once the child has any * lesson or enrolment history: their id is referenced by lessons, payments and @@ -319,6 +365,26 @@ class GuardianService { return $out; } + /** + * The account holder's own details, as the profile screen's form needs them. + * The counterpart to {@see children()} for the person reading the page. + * + * `is_student` is the positive of {@see META_GUARDIAN_ONLY} — see + * {@see updateSelf()}, which reads it back the same way round. + * + * @return array{name: string, email: string, birth_year: string, is_student: bool} + */ + public function accountHolder( int $userId ): array { + $user = get_userdata( $userId ); + + return [ + 'name' => UserName::format( $user instanceof \WP_User ? $user : null, $userId ), + 'email' => $user instanceof \WP_User ? $user->user_email : '', + 'birth_year' => $this->birthYear( $userId ), + 'is_student' => ! self::isGuardianOnly( $userId ), + ]; + } + /** * The guardian behind a child, or null when the student books for themselves. * diff --git a/templates/frontend/family-page.php b/templates/frontend/family-page.php index d5f26ae..482739e 100644 --- a/templates/frontend/family-page.php +++ b/templates/frontend/family-page.php @@ -8,6 +8,7 @@ if (! defined('ABSPATH')) { } /** + * @var array{name: string, email: string, birth_year: string, is_student: bool} $self The account holder's own details. * @var list $children * @var list<\Unsupervised\Schedular\Registration\Question> $questions Account-scope questions, asked once per child. * @var string $error Validation error from the last submission, if any. @@ -26,6 +27,52 @@ if (! defined('ABSPATH')) { +
+ + + +

+ + + +

+ + +

+ + +

+ + +

+

+ + +

+ +

+ + + +

+ +

+ +

+
+ +

+

diff --git a/tests/Unit/BlockPreviewTest.php b/tests/Unit/BlockPreviewTest.php index 5ddfbb0..dfaacdd 100644 --- a/tests/Unit/BlockPreviewTest.php +++ b/tests/Unit/BlockPreviewTest.php @@ -74,6 +74,23 @@ class BlockPreviewTest extends TestCase self::assertStringContainsString('us-editor-note', $html); } + /** + * The preview is what someone placing the block styles against, so it has to + * show both halves of the page — your own details as well as your students'. + */ + public function testFamilyPreviewShowsTheAccountHoldersDetailsAndTheirStudents(): void + { + $html = BlockPreview::family(); + + self::assertStringContainsString('class="us-family-self"', $html); + self::assertStringContainsString('id="us-own-name"', $html); + self::assertStringContainsString('id="us-own-birth-year"', $html); + self::assertStringContainsString('I take lessons myself', $html); + self::assertStringContainsString('class="us-family-list"', $html); + self::assertStringContainsString('class="us-family-add"', $html); + self::assertStringContainsString('us-editor-note', $html); + } + public function testRegistrationPreviewShowsADisabledSampleForm(): void { $html = BlockPreview::registration(); diff --git a/tests/Unit/Guardian/FamilyPageTest.php b/tests/Unit/Guardian/FamilyPageTest.php index 608ed26..2ce8cb8 100644 --- a/tests/Unit/Guardian/FamilyPageTest.php +++ b/tests/Unit/Guardian/FamilyPageTest.php @@ -76,6 +76,21 @@ class FamilyPageTest extends TestCase return $page; } + /** + * The account holder's own details, which every render reads. + * + * @param array{name?: string, email?: string, birth_year?: string, is_student?: bool} $overrides + */ + private function expectAccountHolder(array $overrides = []): void + { + $this->guardians->shouldReceive('accountHolder')->with(5)->andReturn($overrides + [ + 'name' => 'Grace', + 'email' => 'grace@example.com', + 'birth_year' => '1984', + 'is_student' => true, + ]); + } + /** * A question required of everyone, or of nobody — the shape every question * had before the account holder and the students could differ, and the shape @@ -105,6 +120,7 @@ class FamilyPageTest extends TestCase public function testRenderListsTheGuardiansChildren(): void { + $this->expectAccountHolder(); $this->guardians->shouldReceive('children')->once()->with(5)->andReturn([ ['id' => 42, 'name' => 'Ada', 'birth_year' => '2015', 'relationship' => 'Parent'], ]); @@ -117,6 +133,101 @@ class FamilyPageTest extends TestCase self::assertStringContainsString('Add a student', $html); } + public function testRenderShowsTheAccountHoldersOwnDetails(): void + { + $this->expectAccountHolder(); + $this->guardians->shouldReceive('children')->andReturn([]); + $this->questions->shouldReceive('findByScope')->andReturn([]); + + $html = $this->page->render([]); + + self::assertStringContainsString('Your details', $html); + self::assertStringContainsString('value="Grace"', $html); + self::assertStringContainsString('grace@example.com', $html); + self::assertStringContainsString('value="1984"', $html); + // A student in their own right has the box ticked. + self::assertStringContainsString("checked='checked'", $html); + } + + public function testAGuardianOnlyAccountRendersTheStudentBoxUnticked(): void + { + $this->expectAccountHolder(['is_student' => false]); + $this->guardians->shouldReceive('children')->andReturn([]); + $this->questions->shouldReceive('findByScope')->andReturn([]); + + $html = $this->page->render([]); + + self::assertStringContainsString('name="is_student"', $html); + self::assertStringNotContainsString("checked='checked'", $html); + } + + /** + * The birth-year field must not carry `required`: it is asked of a student + * only, and the browser would otherwise block a guardian who books solely + * for other people from ever saving the form. + */ + public function testTheOwnBirthYearFieldIsNotBrowserRequired(): void + { + $this->expectAccountHolder(['is_student' => false, 'birth_year' => '']); + $this->guardians->shouldReceive('children')->andReturn([]); + $this->questions->shouldReceive('findByScope')->andReturn([]); + + $html = $this->page->render([]); + + self::assertMatchesRegularExpression('/]*name="own_birth_year"(?![^>]*\brequired\b)[^>]*>/', $html); + } + + public function testSavingOwnDetailsDelegatesToTheServiceAndRedirects(): void + { + $_POST = [ + 'us_family_action' => 'self', + 'own_name' => 'Grace H', + 'own_birth_year' => '1984', + 'is_student' => '1', + ]; + + $this->guardians->shouldReceive('updateSelf')->once()->with(5, 'Grace H', '1984', true)->andReturn(null); + + $captured = null; + $this->capturingPage($captured)->maybeHandleSubmit(); + + self::assertSame('https://studio.test/family/?us_family=self', $captured); + } + + /** An unticked checkbox is simply absent from the post — that is the "no". */ + public function testAnAbsentStudentBoxSavesTheAccountAsGuardianOnly(): void + { + $_POST = [ + 'us_family_action' => 'self', + 'own_name' => 'Grace H', + 'own_birth_year' => '', + ]; + + $this->guardians->shouldReceive('updateSelf')->once()->with(5, 'Grace H', '', false)->andReturn(null); + + $captured = null; + $this->capturingPage($captured)->maybeHandleSubmit(); + + self::assertSame('https://studio.test/family/?us_family=self', $captured); + } + + public function testOwnDetailsRefusalIsShownRatherThanRedirected(): void + { + $_POST = ['us_family_action' => 'self', 'own_name' => 'Grace', 'is_student' => '1']; + + $this->guardians->shouldReceive('updateSelf')->once()->andReturn( + new \WP_Error('missing_birth_year', 'Please give your birth year.') + ); + + $captured = null; + $page = $this->capturingPage($captured); + $page->shouldNotReceive('redirect'); + + $page->maybeHandleSubmit(); + + self::assertNull($captured); + } + public function testAddCreatesTheChildRecordsItsAnswersAndRedirects(): void { $_POST = [ @@ -347,6 +458,7 @@ class FamilyPageTest extends TestCase { $_GET = ['us_family' => 'added']; + $this->expectAccountHolder(); $this->guardians->shouldReceive('children')->andReturn([]); $this->questions->shouldReceive('findByScope')->andReturn([]); diff --git a/tests/Unit/Guardian/GuardianServiceTest.php b/tests/Unit/Guardian/GuardianServiceTest.php index 76e9ceb..817d4ff 100644 --- a/tests/Unit/Guardian/GuardianServiceTest.php +++ b/tests/Unit/Guardian/GuardianServiceTest.php @@ -321,6 +321,87 @@ class GuardianServiceTest extends TestCase self::assertSame('2015', $this->meta[42][GuardianService::META_BIRTH_YEAR]); } + public function testUpdateSelfRenamesAndStoresTheBirthYear(): void + { + $this->meta[5][GuardianService::META_GUARDIAN_ONLY] = '1'; + + Functions\expect('wp_update_user') + ->once() + ->with(['ID' => 5, 'display_name' => 'Grace H', 'nickname' => 'Grace H']) + ->andReturn(5); + + self::assertNull($this->service->updateSelf(5, 'Grace H', '1984', true)); + + self::assertSame('1984', $this->meta[5][GuardianService::META_BIRTH_YEAR]); + // Saying they take lessons makes them a bookable student again. + self::assertFalse(GuardianService::isGuardianOnly(5)); + } + + public function testUpdateSelfMarksTheAccountGuardianOnly(): void + { + Functions\when('wp_update_user')->justReturn(5); + + self::assertNull($this->service->updateSelf(5, 'Grace H', '', false)); + + self::assertSame('1', $this->meta[5][GuardianService::META_GUARDIAN_ONLY]); + } + + /** + * "I only book for other people" says who books, not "forget my birth year" — + * ticking the box back on should not have cost them what was on file. + */ + public function testUpdateSelfKeepsAStoredBirthYearWhenTheyAreNoLongerAStudent(): void + { + $this->meta[5][GuardianService::META_BIRTH_YEAR] = '1984'; + + Functions\when('wp_update_user')->justReturn(5); + + self::assertNull($this->service->updateSelf(5, 'Grace H', '', false)); + + self::assertSame('1984', $this->meta[5][GuardianService::META_BIRTH_YEAR]); + } + + public function testUpdateSelfRejectsABlankName(): void + { + Functions\expect('wp_update_user')->never(); + + self::assertInstanceOf(\WP_Error::class, $this->service->updateSelf(5, ' ', '1984', true)); + } + + /** + * The browser cannot enforce the year conditionally, so the server is the + * only thing standing between a student and a nonsense age on their record. + * + * @dataProvider unusableBirthYears + */ + public function testUpdateSelfRefusesAnUnusableBirthYearFromAStudent(string $submitted): void + { + Functions\expect('wp_update_user')->never(); + + self::assertInstanceOf(\WP_Error::class, $this->service->updateSelf(5, 'Grace H', $submitted, true)); + } + + public function testAccountHolderReportsTheirOwnDetails(): void + { + $this->meta[5][GuardianService::META_BIRTH_YEAR] = '1984'; + + Functions\when('get_userdata')->justReturn($this->user(5, 'Grace', 'Hopper', email: 'grace@example.test')); + + self::assertSame( + ['name' => 'Grace Hopper', 'email' => 'grace@example.test', 'birth_year' => '1984', 'is_student' => true], + $this->service->accountHolder(5) + ); + } + + public function testAccountHolderReportsAGuardianOnlyAccountAsNotAStudent(): void + { + $this->meta[5][GuardianService::META_GUARDIAN_ONLY] = '1'; + + Functions\when('get_userdata')->justReturn($this->user(5, 'Grace', 'Hopper', email: 'grace@example.test')); + + self::assertFalse($this->service->accountHolder(5)['is_student']); + } + public function testRemoveChildUnlinksAndDeletesAChildWithNoHistory(): void { $this->guardians->shouldReceive('isGuardianOf')->with(5, 42)->andReturn(true);