Add open student registration with email confirmation and approval
CI / Tests (PHP 8.1) (pull_request) Successful in 1m18s
CI / Tests (PHP 8.2) (pull_request) Successful in 1m18s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 3m20s
CI / Coding Standards (pull_request) Successful in 3m25s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m33s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.1) (pull_request) Successful in 1m18s
CI / Tests (PHP 8.2) (pull_request) Successful in 1m18s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 3m20s
CI / Coding Standards (pull_request) Successful in 3m25s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m33s
CI / Build Plugin Zip (pull_request) Skipped
Students could previously join by invite only. Add an optional self-approval mode, toggled from Studio Settings → Registration: anyone may sign up on the existing [us_student_register] page, confirm their email via a tokenised link, and then be approved by a studio admin before the account is usable. - Enabling the toggle mirrors WordPress's own membership settings (users_can_register + default_role = us_student) and snapshots their previous values so disabling restores them. - WordPress's native registration form is blocked while open registration is on (login_init redirect + registration_errors fail-safe + register_url) so it cannot bypass signup policy acceptance. - Pending accounts: unconfirmed email cannot log in; confirmed but unapproved can log in but the booking capability is withheld and the booking page shows an "awaiting approval" screen. - Approve/reject from Students → Pending Students; reject hard-deletes the account so the email is freed to re-apply. - Invite registration is unchanged; both modes coexist. Account lifecycle lives in user meta (RegistrationStatus); no new tables. Closes #63 Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular\Tests\Unit\Auth;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use Unsupervised\Schedular\Auth\EmailConfirmationHandler;
|
||||
use Unsupervised\Schedular\Auth\RegistrationController;
|
||||
use Unsupervised\Schedular\Auth\RegistrationMailer;
|
||||
use Unsupervised\Schedular\Payment\StudioSettings;
|
||||
use Unsupervised\Schedular\Tests\Unit\TestCase;
|
||||
|
||||
class EmailConfirmationHandlerTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Functions\when('wp_unslash')->alias(static fn ($v) => $v);
|
||||
Functions\when('sanitize_key')->alias(static fn ($v) => $v);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($_REQUEST['action']);
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
private function handler(): EmailConfirmationHandler
|
||||
{
|
||||
return new EmailConfirmationHandler(new StudioSettings(), new RegistrationMailer());
|
||||
}
|
||||
|
||||
private function stubMode(string $mode): void
|
||||
{
|
||||
Functions\when('get_option')->alias(static function (string $name, $default = false) use ($mode) {
|
||||
if ($name === StudioSettings::OPT_REGISTRATION_MODE) {
|
||||
return $mode;
|
||||
}
|
||||
if ($name === RegistrationController::OPTION_PAGE) {
|
||||
return 5;
|
||||
}
|
||||
return $default;
|
||||
});
|
||||
}
|
||||
|
||||
public function testRegisterUrlPassesThroughWhenClosed(): void
|
||||
{
|
||||
$this->stubMode(StudioSettings::MODE_INVITE);
|
||||
|
||||
self::assertSame('http://wp/register', $this->handler()->registerUrl('http://wp/register'));
|
||||
}
|
||||
|
||||
public function testRegisterUrlPointsAtRegistrationPageWhenOpen(): void
|
||||
{
|
||||
$this->stubMode(StudioSettings::MODE_SELF_APPROVAL);
|
||||
Functions\when('get_permalink')->justReturn('http://studio.test/register');
|
||||
|
||||
self::assertSame('http://studio.test/register', $this->handler()->registerUrl('http://wp/register'));
|
||||
}
|
||||
|
||||
public function testBlockNativeRegistrationIsNoOpWhenClosed(): void
|
||||
{
|
||||
$this->stubMode(StudioSettings::MODE_INVITE);
|
||||
$_REQUEST['action'] = 'register';
|
||||
|
||||
// Must not redirect/exit when open registration is off.
|
||||
Functions\expect('wp_safe_redirect')->never();
|
||||
|
||||
$this->handler()->blockNativeRegistration();
|
||||
|
||||
unset($_REQUEST['action']);
|
||||
self::assertTrue(true);
|
||||
}
|
||||
|
||||
public function testBlockNativeRegistrationIgnoresOtherActions(): void
|
||||
{
|
||||
$this->stubMode(StudioSettings::MODE_SELF_APPROVAL);
|
||||
$_REQUEST['action'] = 'lostpassword';
|
||||
|
||||
Functions\expect('wp_safe_redirect')->never();
|
||||
|
||||
$this->handler()->blockNativeRegistration();
|
||||
|
||||
unset($_REQUEST['action']);
|
||||
self::assertTrue(true);
|
||||
}
|
||||
|
||||
public function testBlockRegistrationErrorsRejectsWhenOpen(): void
|
||||
{
|
||||
$this->stubMode(StudioSettings::MODE_SELF_APPROVAL);
|
||||
|
||||
$errors = $this->handler()->blockRegistrationErrors(new \WP_Error());
|
||||
|
||||
self::assertSame('us_registration_redirect', $errors->get_error_code());
|
||||
}
|
||||
|
||||
public function testBlockRegistrationErrorsPassesThroughWhenClosed(): void
|
||||
{
|
||||
$this->stubMode(StudioSettings::MODE_INVITE);
|
||||
|
||||
$errors = $this->handler()->blockRegistrationErrors(new \WP_Error());
|
||||
|
||||
self::assertSame('', $errors->get_error_code());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular\Tests\Unit\Auth;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use Mockery;
|
||||
use Unsupervised\Schedular\Auth\RegistrationApprovalController;
|
||||
use Unsupervised\Schedular\Auth\RegistrationMailer;
|
||||
use Unsupervised\Schedular\Auth\RegistrationStatus;
|
||||
use Unsupervised\Schedular\Tests\Unit\TestCase;
|
||||
|
||||
class RegistrationApprovalControllerTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Functions\when('wp_unslash')->alias(static fn ($v) => $v);
|
||||
Functions\when('sanitize_key')->alias(static fn ($v) => $v);
|
||||
Functions\when('absint')->alias(static fn ($v) => (int) $v);
|
||||
$_POST = [];
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_POST = [];
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testRenderPageDiesWithoutCapability(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
Functions\when('wp_die')->alias(static function (): void {
|
||||
throw new \RuntimeException('wp_die');
|
||||
});
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
|
||||
(new RegistrationApprovalController(Mockery::mock(RegistrationMailer::class)))->renderPage();
|
||||
}
|
||||
|
||||
public function testApproveClearsPendingMetaAndEmailsStudent(): void
|
||||
{
|
||||
$_POST = [ 'usc_action' => 'approve', 'user_id' => 7 ];
|
||||
|
||||
Functions\when('get_user_meta')->justReturn('1'); // awaiting approval
|
||||
Functions\expect('delete_user_meta')->atLeast()->once();
|
||||
|
||||
$user = Mockery::mock(\WP_User::class);
|
||||
Functions\when('get_user_by')->justReturn($user);
|
||||
|
||||
$mailer = Mockery::mock(RegistrationMailer::class);
|
||||
$mailer->shouldReceive('sendApproved')->once()->with($user);
|
||||
|
||||
$this->handleAction(new RegistrationApprovalController($mailer));
|
||||
}
|
||||
|
||||
public function testRejectEmailsAndHardDeletesTheAccount(): void
|
||||
{
|
||||
$_POST = [ 'usc_action' => 'reject', 'user_id' => 7 ];
|
||||
|
||||
Functions\when('get_user_meta')->justReturn('1'); // awaiting approval
|
||||
|
||||
$user = Mockery::mock(\WP_User::class);
|
||||
$user->user_email = '[email protected]';
|
||||
Functions\when('get_user_by')->justReturn($user);
|
||||
|
||||
$mailer = Mockery::mock(RegistrationMailer::class);
|
||||
$mailer->shouldReceive('sendRejected')->once()->with('[email protected]');
|
||||
|
||||
Functions\expect('wp_delete_user')->once()->with(7);
|
||||
|
||||
$this->handleAction(new RegistrationApprovalController($mailer));
|
||||
}
|
||||
|
||||
public function testIgnoresUserNotAwaitingApproval(): void
|
||||
{
|
||||
$_POST = [ 'usc_action' => 'approve', 'user_id' => 7 ];
|
||||
|
||||
Functions\when('get_user_meta')->justReturn(''); // not pending
|
||||
|
||||
$mailer = Mockery::mock(RegistrationMailer::class);
|
||||
$mailer->shouldReceive('sendApproved')->never();
|
||||
|
||||
$this->handleAction(new RegistrationApprovalController($mailer));
|
||||
|
||||
// isAwaitingApproval guarded the action; nothing happened.
|
||||
self::assertFalse(RegistrationStatus::isAwaitingApproval(7));
|
||||
}
|
||||
|
||||
private function handleAction(RegistrationApprovalController $controller): void
|
||||
{
|
||||
$method = new \ReflectionMethod(RegistrationApprovalController::class, 'handleAction');
|
||||
$method->invoke($controller);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular\Tests\Unit\Auth;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use Mockery;
|
||||
use Unsupervised\Schedular\Auth\RegistrationLoginGate;
|
||||
use Unsupervised\Schedular\Auth\RegistrationStatus;
|
||||
use Unsupervised\Schedular\Auth\RoleManager;
|
||||
use Unsupervised\Schedular\Tests\Unit\TestCase;
|
||||
|
||||
class RegistrationLoginGateTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Stub get_user_meta to describe a single account's pending state.
|
||||
*/
|
||||
private function stubMeta(string $awaiting, string $confirmed): void
|
||||
{
|
||||
Functions\when('get_user_meta')->alias(static function (int $id, string $key) use ($awaiting, $confirmed) {
|
||||
if ($key === RegistrationStatus::META_AWAITING_APPROVAL) {
|
||||
return $awaiting;
|
||||
}
|
||||
if ($key === RegistrationStatus::META_EMAIL_CONFIRMED) {
|
||||
return $confirmed;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
}
|
||||
|
||||
private function user(int $id): \WP_User
|
||||
{
|
||||
$user = Mockery::mock(\WP_User::class);
|
||||
$user->ID = $id;
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function testBlocksLoginWhileEmailUnconfirmed(): void
|
||||
{
|
||||
$this->stubMeta('1', '');
|
||||
|
||||
$result = (new RegistrationLoginGate())->blockUnconfirmed($this->user(7));
|
||||
|
||||
self::assertInstanceOf(\WP_Error::class, $result);
|
||||
self::assertSame('us_email_unconfirmed', $result->get_error_code());
|
||||
}
|
||||
|
||||
public function testAllowsLoginWhenConfirmedButAwaitingApproval(): void
|
||||
{
|
||||
$this->stubMeta('1', '1');
|
||||
|
||||
$user = $this->user(7);
|
||||
|
||||
self::assertSame($user, (new RegistrationLoginGate())->blockUnconfirmed($user));
|
||||
}
|
||||
|
||||
public function testAllowsLoginForApprovedStudent(): void
|
||||
{
|
||||
$this->stubMeta('', '1');
|
||||
|
||||
$user = $this->user(7);
|
||||
|
||||
self::assertSame($user, (new RegistrationLoginGate())->blockUnconfirmed($user));
|
||||
}
|
||||
|
||||
public function testIgnoresUsersWithNoPendingMeta(): void
|
||||
{
|
||||
// Invite/admin-created students carry no meta at all.
|
||||
$this->stubMeta('', '');
|
||||
|
||||
$user = $this->user(7);
|
||||
|
||||
self::assertSame($user, (new RegistrationLoginGate())->blockUnconfirmed($user));
|
||||
}
|
||||
|
||||
public function testPassesThroughAnEarlierError(): void
|
||||
{
|
||||
$error = new \WP_Error('some_earlier_error', 'nope');
|
||||
|
||||
self::assertSame($error, (new RegistrationLoginGate())->blockUnconfirmed($error));
|
||||
}
|
||||
|
||||
public function testWithholdsBookingCapWhileAwaitingApproval(): void
|
||||
{
|
||||
$this->stubMeta('1', '1');
|
||||
|
||||
$allcaps = [ RoleManager::CAP_BOOK_LESSON => true, 'read' => true ];
|
||||
|
||||
$result = (new RegistrationLoginGate())
|
||||
->withholdBookingWhilePending($allcaps, [], [], $this->user(7));
|
||||
|
||||
self::assertArrayNotHasKey(RoleManager::CAP_BOOK_LESSON, $result);
|
||||
self::assertTrue($result['read']);
|
||||
}
|
||||
|
||||
public function testKeepsBookingCapForApprovedStudent(): void
|
||||
{
|
||||
$this->stubMeta('', '1');
|
||||
|
||||
$allcaps = [ RoleManager::CAP_BOOK_LESSON => true ];
|
||||
|
||||
$result = (new RegistrationLoginGate())
|
||||
->withholdBookingWhilePending($allcaps, [], [], $this->user(7));
|
||||
|
||||
self::assertTrue($result[RoleManager::CAP_BOOK_LESSON]);
|
||||
}
|
||||
|
||||
public function testLeavesCapsUntouchedForNonUserArg(): void
|
||||
{
|
||||
$allcaps = [ RoleManager::CAP_BOOK_LESSON => true ];
|
||||
|
||||
$result = (new RegistrationLoginGate())
|
||||
->withholdBookingWhilePending($allcaps, [], [], null);
|
||||
|
||||
self::assertSame($allcaps, $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular\Tests\Unit\Auth;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use Mockery;
|
||||
use Unsupervised\Schedular\Auth\RegistrationMailer;
|
||||
use Unsupervised\Schedular\Tests\Unit\TestCase;
|
||||
|
||||
class RegistrationMailerTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Functions\when('get_bloginfo')->justReturn('Test Studio');
|
||||
Functions\when('wp_login_url')->justReturn('http://example.test/login');
|
||||
}
|
||||
|
||||
private function user(string $email): \WP_User
|
||||
{
|
||||
$user = Mockery::mock(\WP_User::class);
|
||||
$user->user_email = $email;
|
||||
$user->display_name = 'Ada';
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function testSendConfirmationEmailsTheStudent(): void
|
||||
{
|
||||
Functions\expect('wp_mail')
|
||||
->once()
|
||||
->with(
|
||||
'[email protected]',
|
||||
Mockery::type('string'),
|
||||
Mockery::on(static fn (string $body): bool => str_contains($body, 'http://confirm.test'))
|
||||
)
|
||||
->andReturn(true);
|
||||
|
||||
self::assertTrue((new RegistrationMailer())->sendConfirmation($this->user('[email protected]'), 'http://confirm.test'));
|
||||
}
|
||||
|
||||
public function testSendConfirmationReturnsFalseWithoutRecipient(): void
|
||||
{
|
||||
self::assertFalse((new RegistrationMailer())->sendConfirmation($this->user(''), 'http://confirm.test'));
|
||||
}
|
||||
|
||||
public function testNotifyAdminsPendingUsesAdminEmail(): void
|
||||
{
|
||||
Functions\when('get_option')->alias(static fn (string $name) => $name === 'admin_email' ? '[email protected]' : '');
|
||||
|
||||
Functions\expect('wp_mail')
|
||||
->once()
|
||||
->with('[email protected]', Mockery::type('string'), Mockery::type('string'))
|
||||
->andReturn(true);
|
||||
|
||||
self::assertTrue((new RegistrationMailer())->notifyAdminsPending($this->user('[email protected]')));
|
||||
}
|
||||
|
||||
public function testSendApprovedEmailsTheStudent(): void
|
||||
{
|
||||
Functions\expect('wp_mail')
|
||||
->once()
|
||||
->with('[email protected]', Mockery::type('string'), Mockery::type('string'))
|
||||
->andReturn(true);
|
||||
|
||||
self::assertTrue((new RegistrationMailer())->sendApproved($this->user('[email protected]')));
|
||||
}
|
||||
|
||||
public function testSendRejectedEmailsTheAddress(): void
|
||||
{
|
||||
Functions\expect('wp_mail')
|
||||
->once()
|
||||
->with('[email protected]', Mockery::type('string'), Mockery::type('string'))
|
||||
->andReturn(true);
|
||||
|
||||
self::assertTrue((new RegistrationMailer())->sendRejected('[email protected]'));
|
||||
}
|
||||
|
||||
public function testSendRejectedReturnsFalseWithoutRecipient(): void
|
||||
{
|
||||
self::assertFalse((new RegistrationMailer())->sendRejected(''));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular\Tests\Unit\Auth;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use Mockery;
|
||||
use Unsupervised\Schedular\Auth\Invite;
|
||||
use Unsupervised\Schedular\Auth\InviteRepository;
|
||||
use Unsupervised\Schedular\Auth\RegistrationMailer;
|
||||
use Unsupervised\Schedular\Auth\RegistrationPage;
|
||||
use Unsupervised\Schedular\Payment\StudioSettings;
|
||||
use Unsupervised\Schedular\Policy\AcceptanceRepository;
|
||||
use Unsupervised\Schedular\Policy\Policy;
|
||||
use Unsupervised\Schedular\Policy\PolicyRepository;
|
||||
use Unsupervised\Schedular\Policy\PolicyVersion;
|
||||
use Unsupervised\Schedular\Policy\PolicyVersionRepository;
|
||||
use Unsupervised\Schedular\Tests\Unit\TestCase;
|
||||
|
||||
class RegistrationPageTest extends TestCase
|
||||
{
|
||||
/** @var array<string, mixed> */
|
||||
private array $ctx;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Functions\when('wp_unslash')->alias(static fn ($v) => $v);
|
||||
Functions\when('sanitize_text_field')->alias(static fn ($v) => $v);
|
||||
Functions\when('sanitize_email')->alias(static fn ($v) => $v);
|
||||
Functions\when('current_time')->justReturn('2024-01-01 00:00:00');
|
||||
|
||||
$invites = Mockery::mock(InviteRepository::class);
|
||||
$policies = Mockery::mock(PolicyRepository::class);
|
||||
$policies->shouldReceive('findForScope')->andReturn([])->byDefault();
|
||||
|
||||
$this->ctx = [
|
||||
'invites' => $invites,
|
||||
'policies' => $policies,
|
||||
'mailer' => Mockery::mock(RegistrationMailer::class),
|
||||
'settings' => Mockery::mock(StudioSettings::class),
|
||||
];
|
||||
|
||||
$this->ctx['page'] = new RegistrationPage(
|
||||
$invites,
|
||||
$policies,
|
||||
Mockery::mock(PolicyVersionRepository::class),
|
||||
Mockery::mock(AcceptanceRepository::class),
|
||||
$this->ctx['settings'],
|
||||
$this->ctx['mailer'],
|
||||
);
|
||||
|
||||
$_POST = [];
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_POST = [];
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
private function submit(?Invite $invite, bool $open): string
|
||||
{
|
||||
$method = new \ReflectionMethod(RegistrationPage::class, 'handleSubmit');
|
||||
|
||||
return (string) $method->invoke($this->ctx['page'], $invite, $open);
|
||||
}
|
||||
|
||||
public function testInviteBranchCreatesAndLogsInTheStudent(): void
|
||||
{
|
||||
$_POST = [ 'password' => 'password123', 'display_name' => 'Ada' ];
|
||||
|
||||
Functions\when('email_exists')->justReturn(false);
|
||||
Functions\when('wp_insert_user')->justReturn(42);
|
||||
Functions\when('is_wp_error')->justReturn(false);
|
||||
|
||||
$this->ctx['invites']->shouldReceive('markAccepted')->once();
|
||||
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');
|
||||
|
||||
self::assertSame('invite', $this->submit($invite, false));
|
||||
}
|
||||
|
||||
public function testOpenBranchCreatesPendingWithoutLoginAndEmails(): void
|
||||
{
|
||||
$_POST = [ 'password' => 'password123', 'display_name' => 'Ada', 'email' => '[email protected]' ];
|
||||
|
||||
Functions\when('is_email')->justReturn(true);
|
||||
Functions\when('email_exists')->justReturn(false);
|
||||
Functions\when('wp_insert_user')->justReturn(42);
|
||||
Functions\when('is_wp_error')->justReturn(false);
|
||||
// markPending internals
|
||||
Functions\when('wp_generate_password')->justReturn('rawtok');
|
||||
Functions\when('update_user_meta')->justReturn(true);
|
||||
// confirmUrl internals
|
||||
Functions\when('get_option')->justReturn(0);
|
||||
Functions\when('home_url')->justReturn('http://home.test/');
|
||||
Functions\when('add_query_arg')->alias(static fn (string $k, string $v, string $u): string => $u . '?' . $k . '=' . $v);
|
||||
|
||||
$user = Mockery::mock(\WP_User::class);
|
||||
Functions\when('get_user_by')->justReturn($user);
|
||||
|
||||
$this->ctx['mailer']->shouldReceive('sendConfirmation')->once()->with($user, Mockery::type('string'));
|
||||
// No invite acceptance and no auto-login in the open branch.
|
||||
$this->ctx['invites']->shouldReceive('markAccepted')->never();
|
||||
Functions\expect('wp_set_auth_cookie')->never();
|
||||
|
||||
self::assertSame('confirm', $this->submit(null, true));
|
||||
}
|
||||
|
||||
public function testClosedModeWithoutInviteReturnsError(): void
|
||||
{
|
||||
$result = $this->submit(null, false);
|
||||
|
||||
self::assertNotSame('invite', $result);
|
||||
self::assertNotSame('confirm', $result);
|
||||
self::assertNotSame('', $result);
|
||||
}
|
||||
|
||||
public function testRejectsWhenARequiredPolicyIsUnaccepted(): void
|
||||
{
|
||||
$_POST = [ 'password' => 'password123', 'display_name' => 'Ada', 'email' => '[email protected]' ];
|
||||
|
||||
Functions\when('is_email')->justReturn(true);
|
||||
|
||||
$policy = new Policy(title: 'Terms', slug: 'terms', currentVersionId: 3);
|
||||
$version = new PolicyVersion(policyId: 1, versionNumber: 1, status: PolicyVersion::STATUS_PUBLISHED, id: 3);
|
||||
|
||||
$this->ctx['policies']->shouldReceive('findForScope')->andReturn([ $policy ]);
|
||||
$versionRepo = (new \ReflectionProperty(RegistrationPage::class, 'versions'))->getValue($this->ctx['page']);
|
||||
$versionRepo->shouldReceive('findById')->with(3)->andReturn($version);
|
||||
|
||||
$result = $this->submit(null, true);
|
||||
|
||||
self::assertNotSame('confirm', $result);
|
||||
self::assertNotSame('', $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular\Tests\Unit\Auth;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use Unsupervised\Schedular\Auth\RegistrationStatus;
|
||||
use Unsupervised\Schedular\Tests\Unit\TestCase;
|
||||
|
||||
class RegistrationStatusTest extends TestCase
|
||||
{
|
||||
public function testMarkPendingIssuesHashedTokenAndSetsFlags(): void
|
||||
{
|
||||
Functions\when('wp_generate_password')->justReturn('rawtoken');
|
||||
|
||||
Functions\expect('update_user_meta')
|
||||
->once()
|
||||
->with(7, RegistrationStatus::META_AWAITING_APPROVAL, '1');
|
||||
Functions\expect('update_user_meta')
|
||||
->once()
|
||||
->with(7, RegistrationStatus::META_CONFIRM_TOKEN, hash('sha256', 'rawtoken'));
|
||||
Functions\expect('update_user_meta')
|
||||
->once()
|
||||
->with(7, RegistrationStatus::META_CONFIRM_EXPIRES, \Mockery::type('string'));
|
||||
|
||||
self::assertSame('rawtoken', RegistrationStatus::markPending(7));
|
||||
}
|
||||
|
||||
public function testConfirmEmailSetsFlagAndClearsToken(): void
|
||||
{
|
||||
Functions\expect('update_user_meta')
|
||||
->once()
|
||||
->with(7, RegistrationStatus::META_EMAIL_CONFIRMED, '1');
|
||||
Functions\expect('delete_user_meta')->once()->with(7, RegistrationStatus::META_CONFIRM_TOKEN);
|
||||
Functions\expect('delete_user_meta')->once()->with(7, RegistrationStatus::META_CONFIRM_EXPIRES);
|
||||
|
||||
RegistrationStatus::confirmEmail(7);
|
||||
}
|
||||
|
||||
public function testApproveClearsPendingFlags(): void
|
||||
{
|
||||
Functions\expect('delete_user_meta')->once()->with(7, RegistrationStatus::META_AWAITING_APPROVAL);
|
||||
Functions\expect('delete_user_meta')->once()->with(7, RegistrationStatus::META_CONFIRM_TOKEN);
|
||||
Functions\expect('delete_user_meta')->once()->with(7, RegistrationStatus::META_CONFIRM_EXPIRES);
|
||||
|
||||
RegistrationStatus::approve(7);
|
||||
}
|
||||
|
||||
public function testAwaitingApprovalAndEmailConfirmedReadMeta(): void
|
||||
{
|
||||
Functions\when('get_user_meta')->alias(static function (int $id, string $key) {
|
||||
return $key === RegistrationStatus::META_AWAITING_APPROVAL ? '1' : '';
|
||||
});
|
||||
|
||||
self::assertTrue(RegistrationStatus::isAwaitingApproval(7));
|
||||
self::assertFalse(RegistrationStatus::emailConfirmed(7));
|
||||
}
|
||||
|
||||
public function testUserIdForTokenLooksUpByHashOnly(): void
|
||||
{
|
||||
Functions\expect('get_users')
|
||||
->once()
|
||||
->with(\Mockery::on(static fn (array $args): bool =>
|
||||
$args['meta_key'] === RegistrationStatus::META_CONFIRM_TOKEN
|
||||
&& $args['meta_value'] === hash('sha256', 'rawtoken')))
|
||||
->andReturn([9]);
|
||||
|
||||
self::assertSame(9, RegistrationStatus::userIdForToken('rawtoken'));
|
||||
}
|
||||
|
||||
public function testUserIdForTokenReturnsNullWhenNoMatch(): void
|
||||
{
|
||||
Functions\when('get_users')->justReturn([]);
|
||||
|
||||
self::assertNull(RegistrationStatus::userIdForToken('rawtoken'));
|
||||
}
|
||||
|
||||
public function testEmptyTokenShortCircuits(): void
|
||||
{
|
||||
// get_users must never be called for an empty token.
|
||||
Functions\expect('get_users')->never();
|
||||
|
||||
self::assertNull(RegistrationStatus::userIdForToken(''));
|
||||
}
|
||||
|
||||
public function testTokenExpiredWhenExpiryPassed(): void
|
||||
{
|
||||
Functions\when('get_user_meta')->justReturn('2020-01-01 00:00:00');
|
||||
|
||||
self::assertTrue(RegistrationStatus::isTokenExpired(7, '2020-01-02 00:00:00'));
|
||||
}
|
||||
|
||||
public function testTokenNotExpiredWhenExpiryInFuture(): void
|
||||
{
|
||||
Functions\when('get_user_meta')->justReturn('2020-01-03 00:00:00');
|
||||
|
||||
self::assertFalse(RegistrationStatus::isTokenExpired(7, '2020-01-02 00:00:00'));
|
||||
}
|
||||
|
||||
public function testMissingExpiryTreatedAsExpired(): void
|
||||
{
|
||||
Functions\when('get_user_meta')->justReturn('');
|
||||
|
||||
self::assertTrue(RegistrationStatus::isTokenExpired(7, '2020-01-02 00:00:00'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular\Tests\Unit\Payment;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use Unsupervised\Schedular\Auth\RoleManager;
|
||||
use Unsupervised\Schedular\Payment\StudioSettings;
|
||||
use Unsupervised\Schedular\Tests\Unit\TestCase;
|
||||
|
||||
class StudioSettingsTest extends TestCase
|
||||
{
|
||||
public function testRegistrationModeDefaultsToInvite(): void
|
||||
{
|
||||
Functions\when('get_option')->alias(static fn (string $name, $default) => $default);
|
||||
|
||||
$settings = new StudioSettings();
|
||||
|
||||
self::assertSame(StudioSettings::MODE_INVITE, $settings->registrationMode());
|
||||
self::assertFalse($settings->openRegistrationEnabled());
|
||||
}
|
||||
|
||||
public function testOpenRegistrationEnabledWhenStored(): void
|
||||
{
|
||||
Functions\when('get_option')->alias(static fn (string $name) =>
|
||||
$name === StudioSettings::OPT_REGISTRATION_MODE ? StudioSettings::MODE_SELF_APPROVAL : '');
|
||||
|
||||
self::assertTrue((new StudioSettings())->openRegistrationEnabled());
|
||||
}
|
||||
|
||||
public function testEnablingMirrorsCoreOptionsAndSnapshots(): void
|
||||
{
|
||||
Functions\when('get_option')->alias(static function (string $name, $default = false) {
|
||||
return match ($name) {
|
||||
StudioSettings::OPT_REGISTRATION_MODE => StudioSettings::MODE_INVITE, // currently closed
|
||||
'users_can_register' => false, // snapshot as '0'
|
||||
'default_role' => 'contributor',
|
||||
default => $default,
|
||||
};
|
||||
});
|
||||
|
||||
Functions\expect('update_option')->once()->with(StudioSettings::OPT_PREV_USERS_CAN_REGISTER, '0');
|
||||
Functions\expect('update_option')->once()->with(StudioSettings::OPT_PREV_DEFAULT_ROLE, 'contributor');
|
||||
Functions\expect('update_option')->once()->with('users_can_register', '1');
|
||||
Functions\expect('update_option')->once()->with('default_role', RoleManager::STUDENT);
|
||||
Functions\expect('update_option')->once()->with(StudioSettings::OPT_REGISTRATION_MODE, StudioSettings::MODE_SELF_APPROVAL);
|
||||
|
||||
$this->applyMode(true);
|
||||
}
|
||||
|
||||
public function testDisablingRestoresSnapshot(): void
|
||||
{
|
||||
Functions\when('get_option')->alias(static function (string $name, $default = false) {
|
||||
return match ($name) {
|
||||
StudioSettings::OPT_REGISTRATION_MODE => StudioSettings::MODE_SELF_APPROVAL, // currently open
|
||||
StudioSettings::OPT_PREV_USERS_CAN_REGISTER => '1',
|
||||
StudioSettings::OPT_PREV_DEFAULT_ROLE => 'contributor',
|
||||
default => $default,
|
||||
};
|
||||
});
|
||||
|
||||
Functions\expect('update_option')->once()->with('users_can_register', '1');
|
||||
Functions\expect('update_option')->once()->with('default_role', 'contributor');
|
||||
Functions\expect('delete_option')->once()->with(StudioSettings::OPT_PREV_USERS_CAN_REGISTER);
|
||||
Functions\expect('delete_option')->once()->with(StudioSettings::OPT_PREV_DEFAULT_ROLE);
|
||||
Functions\expect('update_option')->once()->with(StudioSettings::OPT_REGISTRATION_MODE, StudioSettings::MODE_INVITE);
|
||||
|
||||
$this->applyMode(false);
|
||||
}
|
||||
|
||||
public function testNoTransitionLeavesCoreOptionsUntouched(): void
|
||||
{
|
||||
// Already open, asked to enable again: nothing should be written.
|
||||
Functions\when('get_option')->alias(static fn (string $name, $default = false) =>
|
||||
$name === StudioSettings::OPT_REGISTRATION_MODE ? StudioSettings::MODE_SELF_APPROVAL : $default);
|
||||
|
||||
Functions\expect('update_option')->never();
|
||||
Functions\expect('delete_option')->never();
|
||||
|
||||
$this->applyMode(true);
|
||||
}
|
||||
|
||||
private function applyMode(bool $enable): void
|
||||
{
|
||||
$method = new \ReflectionMethod(StudioSettings::class, 'applyRegistrationMode');
|
||||
$method->invoke(new StudioSettings(), $enable);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user