Let the studio register the students who cannot register themselves
CI / No Debug Code (pull_request) Successful in 4s
CI / Tests (PHP 8.2) (pull_request) Successful in 50s
CI / Tests (PHP 8.1) (pull_request) Successful in 1m3s
CI / Tests (PHP 8.5) (pull_request) Successful in 2m48s
CI / Tests (PHP 8.3) (pull_request) Successful in 3m24s
CI / Coding Standards & Static Analysis (pull_request) Successful in 8m21s
CI / Build Plugin Zip (pull_request) Skipped
CI / No Debug Code (pull_request) Successful in 4s
CI / Tests (PHP 8.2) (pull_request) Successful in 50s
CI / Tests (PHP 8.1) (pull_request) Successful in 1m3s
CI / Tests (PHP 8.5) (pull_request) Successful in 2m48s
CI / Tests (PHP 8.3) (pull_request) Successful in 3m24s
CI / Coding Standards & Static Analysis (pull_request) Successful in 8m21s
CI / Build Plugin Zip (pull_request) Skipped
The Book a lesson for a student panel built its picker from the us_student role but vetted the submission with the book_lesson capability. ChildLoginGate and RegistrationLoginGate withhold that capability from accounts that keep the role, so the panel offered every guardian-managed child and every unapproved signup and then refused them — with a message claiming no student had been chosen, and a form cleared of all five fields. Withholding book_lesson stops those accounts registering in their own name. It was never meant to stop the studio acting for them, which is what the panel is for, and for a child is the only route to a lesson besides their guardian. Guard the student role instead, via a new RoleManager::isStudent() shared with every picker and guard on the staff side so the two cannot drift apart again. Group enrolment gets the same predicate: addDirect() and grantAccess() vetted their posted ids not at all, and would enrol an instructor, an administrator, or an account deleted since the page was drawn — raising a real payment against them for a priced class. Keep a refused booking's fields as submitted, reading the form through one LessonController::submittedBooking() so what gets booked and what is shown again cannot disagree about a field name. A booking that succeeds still leaves an empty form, so the next one does not inherit it. Closes #185 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01XunBYk2sFEc1oL14sUiuBU
This commit is contained in:
@@ -5,6 +5,7 @@ namespace Unsupervised\Schedular\Tests\Unit\Booking;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use Mockery;
|
||||
use Unsupervised\Schedular\Auth\RoleManager;
|
||||
use Unsupervised\Schedular\Availability\AvailabilityRepository;
|
||||
use Unsupervised\Schedular\Availability\AvailabilitySlot;
|
||||
use Unsupervised\Schedular\Booking\AdminBooking;
|
||||
@@ -36,10 +37,10 @@ class AdminBookingTest extends TestCase
|
||||
Functions\when('mysql2date')->alias(
|
||||
static fn (string $format, string $date): string => date($format, (int) strtotime($date))
|
||||
);
|
||||
Functions\when('get_userdata')->justReturn(false);
|
||||
// The picker offers holders of the student role, and that is what the guard
|
||||
// accepts; it is exercised on its own below.
|
||||
Functions\when('get_userdata')->alias(fn (int $id): \WP_User => $this->user($id, 'Jane Doe'));
|
||||
Functions\when('get_users')->justReturn([]);
|
||||
// Everyone offered in the picker can book; the guard is exercised on its own.
|
||||
Functions\when('user_can')->justReturn(true);
|
||||
// The staff member doing the booking; stamped on the lesson as booked_by.
|
||||
Functions\when('get_current_user_id')->justReturn(3);
|
||||
|
||||
@@ -175,9 +176,9 @@ class AdminBookingTest extends TestCase
|
||||
self::assertSame('slot_taken', $result->get_error_code());
|
||||
}
|
||||
|
||||
public function testSomeoneWhoCannotBookLessonsIsRefused(): void
|
||||
public function testSomeoneWhoIsNotAStudentIsRefused(): void
|
||||
{
|
||||
Functions\when('user_can')->justReturn(false);
|
||||
Functions\when('get_userdata')->alias(fn (int $id): \WP_User => $this->user($id, 'Jane Doe', [RoleManager::INSTRUCTOR]));
|
||||
$this->availability->shouldReceive('findById')->never();
|
||||
|
||||
$result = $this->admin->book(42, 7, 3, Lesson::RECURRENCE_SINGLE, false, '');
|
||||
@@ -186,6 +187,69 @@ class AdminBookingTest extends TestCase
|
||||
self::assertSame('invalid_student', $result->get_error_code());
|
||||
}
|
||||
|
||||
public function testAnAccountThatNoLongerExistsIsRefused(): void
|
||||
{
|
||||
Functions\when('get_userdata')->justReturn(false);
|
||||
$this->availability->shouldReceive('findById')->never();
|
||||
|
||||
$result = $this->admin->book(42, 7, 3, Lesson::RECURRENCE_SINGLE, false, '');
|
||||
|
||||
self::assertInstanceOf(\WP_Error::class, $result);
|
||||
self::assertSame('invalid_student', $result->get_error_code());
|
||||
}
|
||||
|
||||
public function testNoStudentChosenIsRefusedWithoutLookingAnyoneUp(): void
|
||||
{
|
||||
Functions\expect('get_userdata')->never();
|
||||
$this->availability->shouldReceive('findById')->never();
|
||||
|
||||
$result = $this->admin->book(0, 7, 3, Lesson::RECURRENCE_SINGLE, false, '');
|
||||
|
||||
self::assertInstanceOf(\WP_Error::class, $result);
|
||||
self::assertSame('invalid_student', $result->get_error_code());
|
||||
}
|
||||
|
||||
/**
|
||||
* A child holds the student role but never `book_lesson` — withheld so the
|
||||
* account cannot book in its own name. The studio booking for them is the only
|
||||
* route a child has to a lesson, so it must not be blocked by that.
|
||||
*/
|
||||
public function testBooksForAGuardiansChildWhoCannotBookThemselves(): void
|
||||
{
|
||||
Functions\when('user_can')->justReturn(false);
|
||||
$this->availability->shouldReceive('findById')->with(7)->andReturn($this->slot());
|
||||
$this->offerings->shouldReceive('findById')->with(3)->andReturn($this->offering());
|
||||
$this->availability->shouldReceive('claim')->once()->with(7)->andReturn(true);
|
||||
$this->bookings->shouldReceive('insert')->once()->andReturn(100);
|
||||
$this->payments->shouldReceive('createForRegistration')->once()->andReturn($this->pendingPayment());
|
||||
|
||||
$notice = $this->admin->book(42, 7, 3, Lesson::RECURRENCE_SINGLE, false, '');
|
||||
|
||||
self::assertIsString($notice);
|
||||
self::assertStringContainsString('30 min piano', $notice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same for a self-signup the studio has not approved yet: the front desk can
|
||||
* still get them onto the calendar while the paperwork catches up.
|
||||
*/
|
||||
public function testBooksForAStudentStillAwaitingApproval(): void
|
||||
{
|
||||
Functions\when('user_can')->justReturn(false);
|
||||
$this->availability->shouldReceive('findById')->with(7)->andReturn($this->slot());
|
||||
$this->offerings->shouldReceive('findById')->with(3)->andReturn($this->offering());
|
||||
$this->availability->shouldReceive('claim')->once()->with(7)->andReturn(true);
|
||||
$this->bookings->shouldReceive('insert')->once()->with(Mockery::on(
|
||||
static fn (Lesson $l): bool => 42 === $l->studentId
|
||||
))->andReturn(100);
|
||||
$this->payments->shouldReceive('createForRegistration')->once()->andReturn($this->pendingPayment());
|
||||
|
||||
$notice = $this->admin->book(42, 7, 3, Lesson::RECURRENCE_SINGLE, false, '');
|
||||
|
||||
self::assertIsString($notice);
|
||||
self::assertStringContainsString('pending payment', $notice);
|
||||
}
|
||||
|
||||
public function testATiedTimeCannotBeBookedAsADifferentLessonType(): void
|
||||
{
|
||||
$this->availability->shouldReceive('findById')->with(7)->andReturn($this->slot(offeringId: 3));
|
||||
@@ -299,10 +363,12 @@ class AdminBookingTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
private function user(int $id, string $name): \WP_User
|
||||
/** @param list<string> $roles */
|
||||
private function user(int $id, string $name, array $roles = [RoleManager::STUDENT]): \WP_User
|
||||
{
|
||||
$user = Mockery::mock(\WP_User::class);
|
||||
$user->ID = $id;
|
||||
$user->roles = $roles;
|
||||
$user->first_name = '';
|
||||
$user->last_name = '';
|
||||
$user->nickname = $name;
|
||||
|
||||
Reference in New Issue
Block a user