Fix invite sign-in persistence, add invite-only text option, repair account questions
CI / Tests (PHP 8.1) (pull_request) Successful in 47s
CI / Tests (PHP 8.2) (pull_request) Successful in 46s
CI / No Debug Code (pull_request) Successful in 3s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m37s
CI / PHPStan (pull_request) Successful in 2m52s
CI / Coding Standards (pull_request) Successful in 3m6s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.1) (pull_request) Successful in 47s
CI / Tests (PHP 8.2) (pull_request) Successful in 46s
CI / No Debug Code (pull_request) Successful in 3s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m37s
CI / PHPStan (pull_request) Successful in 2m52s
CI / Coding Standards (pull_request) Successful in 3m6s
CI / Build Plugin Zip (pull_request) Skipped
Three registration fixes reported from live use:
- Accepting an invite now keeps the student signed in. The form was
processed inside render() during the_content, so wp_set_auth_cookie()
ran after headers were sent and the cookie never persisted — the new
student was bounced back to the logged-out registration page. The
submission is now handled on template_redirect (before output) with a
post/redirect/get, so the cookie sticks and the student lands logged in.
- The "registration is by invitation only" message is now customisable via
a new block attribute (inviteOnlyMessage / shortcode invite_only_message),
falling back to the default wording when blank.
- Account-registration questions save again. dbDelta does not reliably
relax a column from NOT NULL to NULL, so sites created before account-
scope questions kept us_questions.offering_id NOT NULL and rejected
account inserts ("Column 'offering_id' cannot be null"). A one-time,
self-healing migration (guarded by its own option, not the version gate)
re-applies the nullable definition on next load.
composer test, composer lint, composer cs all pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
@@ -59,11 +59,14 @@ class RegistrationPageTest extends TestCase
|
||||
'settings' => Mockery::mock(StudioSettings::class),
|
||||
];
|
||||
|
||||
$this->ctx['versions'] = Mockery::mock(PolicyVersionRepository::class);
|
||||
$this->ctx['acceptances'] = Mockery::mock(AcceptanceRepository::class);
|
||||
|
||||
$this->ctx['page'] = new RegistrationPage(
|
||||
$invites,
|
||||
$policies,
|
||||
Mockery::mock(PolicyVersionRepository::class),
|
||||
Mockery::mock(AcceptanceRepository::class),
|
||||
$this->ctx['versions'],
|
||||
$this->ctx['acceptances'],
|
||||
$this->ctx['settings'],
|
||||
$this->ctx['mailer'],
|
||||
$questions,
|
||||
@@ -403,4 +406,98 @@ class RegistrationPageTest extends TestCase
|
||||
|
||||
self::assertSame('invite', $this->submit($invite, false));
|
||||
}
|
||||
|
||||
public function testMaybeHandleSubmitLogsInInviteAndRedirects(): void
|
||||
{
|
||||
$_POST = [ 'us_register' => '1', 'password' => 'password123', 'display_name' => 'Ada' ];
|
||||
$_REQUEST = [ 'us_invite' => 'raw-token' ];
|
||||
|
||||
Functions\when('is_user_logged_in')->justReturn(false);
|
||||
Functions\when('check_admin_referer')->justReturn(true);
|
||||
Functions\when('email_exists')->justReturn(false);
|
||||
Functions\when('wp_insert_user')->justReturn(42);
|
||||
Functions\when('is_wp_error')->justReturn(false);
|
||||
Functions\when('get_permalink')->justReturn('http://home.test/register/');
|
||||
Functions\when('add_query_arg')->alias(static fn (string $k, string $v, string $u): string => $u . '?' . $k . '=' . $v);
|
||||
|
||||
// The cookie must be set here — during template_redirect, before output —
|
||||
// which is the whole point of processing the submit outside render().
|
||||
Functions\expect('wp_set_current_user')->once()->with(42);
|
||||
Functions\expect('wp_set_auth_cookie')->once()->with(42);
|
||||
|
||||
$invite = new Invite(email: '[email protected]', token: 'hash', createdAt: '2024-01-01 00:00:00', id: 9);
|
||||
$this->ctx['invites']->shouldReceive('findByToken')->once()->andReturn($invite);
|
||||
$this->ctx['invites']->shouldReceive('markAccepted')->once();
|
||||
$this->ctx['settings']->shouldReceive('openRegistrationEnabled')->andReturn(false);
|
||||
|
||||
$page = Mockery::mock(
|
||||
RegistrationPage::class,
|
||||
[
|
||||
$this->ctx['invites'],
|
||||
$this->ctx['policies'],
|
||||
$this->ctx['versions'],
|
||||
$this->ctx['acceptances'],
|
||||
$this->ctx['settings'],
|
||||
$this->ctx['mailer'],
|
||||
$this->ctx['questions'],
|
||||
$this->ctx['answers'],
|
||||
$this->ctx['access'],
|
||||
]
|
||||
)->makePartial()->shouldAllowMockingProtectedMethods();
|
||||
|
||||
$captured = '';
|
||||
$page->shouldReceive('redirect')->once()->with(Mockery::on(static function (string $url) use (&$captured): bool {
|
||||
$captured = $url;
|
||||
return true;
|
||||
}));
|
||||
|
||||
$page->maybeHandleSubmit();
|
||||
|
||||
self::assertStringContainsString('us_registered=invite', $captured);
|
||||
}
|
||||
|
||||
public function testMaybeHandleSubmitStoresValidationErrorWithoutRedirecting(): void
|
||||
{
|
||||
// Too-short password: handleSubmit returns an error and no redirect fires.
|
||||
$_POST = [ 'us_register' => '1', 'password' => 'short', 'display_name' => 'Ada' ];
|
||||
|
||||
Functions\when('is_user_logged_in')->justReturn(false);
|
||||
Functions\when('check_admin_referer')->justReturn(true);
|
||||
$this->ctx['settings']->shouldReceive('openRegistrationEnabled')->andReturn(true);
|
||||
|
||||
// A redirect would call exit; reaching the assertion proves none happened.
|
||||
$this->ctx['page']->maybeHandleSubmit();
|
||||
|
||||
$error = (new \ReflectionProperty(RegistrationPage::class, 'submitError'))->getValue($this->ctx['page']);
|
||||
self::assertNotSame('', $error);
|
||||
}
|
||||
|
||||
public function testInviteSuccessRedirectShowsLoggedInWelcome(): void
|
||||
{
|
||||
// After the PRG redirect the student is logged in; the us_registered flag
|
||||
// distinguishes a just-completed signup from an already-logged-in visitor.
|
||||
$_GET = [ 'us_registered' => 'invite' ];
|
||||
Functions\when('is_user_logged_in')->justReturn(true);
|
||||
Functions\when('sanitize_key')->alias(static fn ($v) => strtolower((string) $v));
|
||||
|
||||
$html = $this->ctx['page']->render([]);
|
||||
|
||||
self::assertStringContainsString('us-success', $html);
|
||||
self::assertStringContainsString('now logged in', $html);
|
||||
}
|
||||
|
||||
public function testInviteOnlyMessageCanBeCustomised(): void
|
||||
{
|
||||
// Closed registration and no invite → the invitation-only gate shows.
|
||||
Functions\when('is_user_logged_in')->justReturn(false);
|
||||
Functions\when('sanitize_key')->alias(static fn ($v) => strtolower((string) $v));
|
||||
Functions\when('wp_login_url')->justReturn('http://home.test/wp-login.php');
|
||||
Functions\when('wp_nonce_field')->justReturn('');
|
||||
$this->ctx['settings']->shouldReceive('openRegistrationEnabled')->andReturn(false);
|
||||
|
||||
$html = $this->ctx['page']->render([ 'inviteOnlyMessage' => 'Ask the front desk for a link.' ]);
|
||||
|
||||
self::assertStringContainsString('Ask the front desk for a link.', $html);
|
||||
self::assertStringNotContainsString('by invitation only', $html);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ class BlockRegistrarTest extends TestCase
|
||||
array_keys($registered['us-scheduler/student-login']['attributes'])
|
||||
);
|
||||
self::assertSame(
|
||||
['loginPageId'],
|
||||
['loginPageId', 'inviteOnlyMessage'],
|
||||
array_keys($registered['us-scheduler/student-register']['attributes'])
|
||||
);
|
||||
self::assertSame(
|
||||
|
||||
@@ -212,4 +212,28 @@ class QuestionRepositoryTest extends TestCase
|
||||
|
||||
self::assertTrue($this->repo->delete(4));
|
||||
}
|
||||
|
||||
public function testEnsureOfferingNullableRunsAlterAndReportsSuccess(): void
|
||||
{
|
||||
$this->db->shouldReceive('prepare')
|
||||
->once()
|
||||
->with(Mockery::pattern('/ALTER TABLE %i MODIFY offering_id .*NULL/'), 'wp_us_questions')
|
||||
->andReturn('ALTER TABLE `wp_us_questions` MODIFY offering_id BIGINT UNSIGNED NULL DEFAULT NULL');
|
||||
|
||||
$this->db->shouldReceive('query')
|
||||
->once()
|
||||
->with('ALTER TABLE `wp_us_questions` MODIFY offering_id BIGINT UNSIGNED NULL DEFAULT NULL')
|
||||
->andReturn(0);
|
||||
|
||||
// A successful DDL query returns 0 rows affected (not false).
|
||||
self::assertTrue($this->repo->ensureOfferingNullable());
|
||||
}
|
||||
|
||||
public function testEnsureOfferingNullableReportsFailureWhenQueryFails(): void
|
||||
{
|
||||
$this->db->shouldReceive('prepare')->once()->andReturn('ALTER ...');
|
||||
$this->db->shouldReceive('query')->once()->andReturn(false);
|
||||
|
||||
self::assertFalse($this->repo->ensureOfferingNullable());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user