Let the account holder edit their own profile details
CI / Tests (PHP 8.1) (pull_request) Successful in 45s
CI / No Debug Code (pull_request) Successful in 2s
CI / Tests (PHP 8.2) (pull_request) Successful in 55s
CI / PHPStan (pull_request) Successful in 2m57s
CI / Coding Standards (pull_request) Successful in 3m3s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m42s
CI / Build Plugin Zip (pull_request) Skipped

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 <[email protected]>
This commit is contained in:
2026-07-30 15:08:05 -03:00
co-authored by Claude Opus 5
parent 325a86f247
commit f97b8a4576
10 changed files with 443 additions and 10 deletions
+2
View File
@@ -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]
+26
View File
@@ -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;
+48 -4
View File
@@ -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`
+23 -4
View File
@@ -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(
'<h4>%s</h4><p class="us-family-self-email">%s <span>[email protected]</span></p>'
. '<p><label for="us-own-name">%s' . self::REQUIRED_MARK . '</label><input type="text" id="us-own-name" value="%s"></p>'
. '<p><label><input type="checkbox" checked disabled> %s</label></p>'
. '<p><label for="us-own-birth-year">%s</label><input type="number" id="us-own-birth-year" placeholder="YYYY"></p>'
. '<p><button type="button" disabled>%s</button></p>',
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(
'<div class="us-family">%s<h3>%s</h3><ul class="us-family-list">%s</ul><form class="us-family-add">%s</form></div>',
self::note( __( 'Editor preview — signed-in guardians see and manage their own students here.', 'unsupervised-schedular' ) ),
'<div class="us-family">%s<h3>%s</h3><form class="us-family-self">%s</form>'
. '<h4>%s</h4><ul class="us-family-list">%s</ul><form class="us-family-add">%s</form></div>',
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
);
+21 -2
View File
@@ -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 => '',
};
}
+66
View File
@@ -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.
*
+47
View File
@@ -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<array{id: int, name: string, birth_year: string, relationship: string}> $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')) {
<p class="us-error" role="alert"><?php echo esc_html($error); ?></p>
<?php endif; ?>
<form method="post" action="" class="us-family-self">
<?php wp_nonce_field('us_family'); ?>
<input type="hidden" name="us_family_action" value="self">
<h4><?php esc_html_e('Your details', 'unsupervised-schedular'); ?></h4>
<?php if ($self['email'] !== '') : ?>
<?php /* Shown, not editable: the address is the account's login, and changing it is a studio-side job. */ ?>
<p class="us-family-self-email">
<?php esc_html_e('Email', 'unsupervised-schedular'); ?>
<span><?php echo esc_html($self['email']); ?></span>
</p>
<?php endif; ?>
<p>
<label for="us-own-name"><?php esc_html_e('Your name', 'unsupervised-schedular'); ?> <span class="us-required" aria-hidden="true">*</span></label>
<input type="text" name="own_name" id="us-own-name" autocomplete="name" required value="<?php echo esc_attr($self['name']); ?>">
</p>
<p>
<label>
<input type="checkbox" name="is_student" value="1"<?php checked($self['is_student']); ?>>
<?php esc_html_e('I take lessons myself', 'unsupervised-schedular'); ?>
</label>
<span class="us-field-hint"><?php esc_html_e('Leave this unticked if you only book for the students below — you will not be offered as a student yourself.', 'unsupervised-schedular'); ?></span>
</p>
<?php
/*
* Asked of a student only, so `required` is deliberately absent: the box
* above is what decides, and the browser cannot be told to enforce a
* field conditionally without JavaScript this page does not load.
* `handleSelf()` enforces it on the server either way.
*/
?>
<p>
<label for="us-own-birth-year"><?php esc_html_e('Your birth year', 'unsupervised-schedular'); ?></label>
<input type="number" name="own_birth_year" id="us-own-birth-year" value="<?php echo esc_attr($self['birth_year']); ?>" min="1900" max="<?php echo esc_attr(current_time('Y')); ?>" step="1" inputmode="numeric" autocomplete="bday-year" placeholder="<?php esc_attr_e('YYYY', 'unsupervised-schedular'); ?>" aria-describedby="us-own-birth-year-hint">
<span class="us-field-hint" id="us-own-birth-year-hint"><?php esc_html_e('Needed only if you take lessons yourself.', 'unsupervised-schedular'); ?></span>
</p>
<p>
<button type="submit"><?php esc_html_e('Save my details', 'unsupervised-schedular'); ?></button>
</p>
</form>
<h4><?php esc_html_e('Your students', 'unsupervised-schedular'); ?></h4>
<?php if (empty($children)) : ?>
<p><?php esc_html_e('You have not added any students yet. Add one below to start booking lessons for them.', 'unsupervised-schedular'); ?></p>
<?php else : ?>
+17
View File
@@ -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();
+112
View File
@@ -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' => '[email protected]',
'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('[email protected]', $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('/<input[^>]*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([]);
@@ -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: '[email protected]'));
self::assertSame(
['name' => 'Grace Hopper', 'email' => '[email protected]', '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: '[email protected]'));
self::assertFalse($this->service->accountHolder(5)['is_student']);
}
public function testRemoveChildUnlinksAndDeletesAChildWithNoHistory(): void
{
$this->guardians->shouldReceive('isGuardianOf')->with(5, 42)->andReturn(true);