justReturn(5); Functions\when('get_userdata')->justReturn((object) ['display_name' => 'Ada Lovelace']); $this->repository = Mockery::mock(OfferingRepository::class); $this->access = Mockery::mock(GroupAccessRepository::class); $this->endpoint = new OfferingEndpoint($this->repository, $this->access); } private function group(int $id, string $access): Offering { return new Offering(instructorId: 3, kind: Offering::KIND_GROUP_CLASS, title: "Class $id", accessMode: $access, id: $id); } public function testIndexReturnsPublicOfferingsOnlyWhenNoGrants(): void { $this->repository->shouldReceive('findAll') ->once() ->with(0, '', Mockery::on(static fn ($v): bool => true === $v), Offering::ACCESS_PUBLIC) ->andReturn([$this->group(1, Offering::ACCESS_PUBLIC)]); $this->access->shouldReceive('findGrantedOfferingIds')->with(5)->andReturn([]); $data = $this->endpoint->index(new \WP_REST_Request())->get_data(); self::assertCount(1, $data); self::assertSame(1, $data[0]['id']); } public function testIndexMergesGrantedInviteOnlyOfferings(): void { $this->repository->shouldReceive('findAll') ->once() ->with(0, '', Mockery::any(), Offering::ACCESS_PUBLIC) ->andReturn([$this->group(1, Offering::ACCESS_PUBLIC)]); $this->access->shouldReceive('findGrantedOfferingIds')->with(5)->andReturn([8]); $this->repository->shouldReceive('findById')->with(8)->andReturn($this->group(8, Offering::ACCESS_INVITE_ONLY)); $data = $this->endpoint->index(new \WP_REST_Request())->get_data(); self::assertSame([1, 8], array_column($data, 'id')); } public function testIndexOmitsGrantedOfferingThatIsNoLongerInviteOnly(): void { $this->repository->shouldReceive('findAll')->andReturn([]); $this->access->shouldReceive('findGrantedOfferingIds')->with(5)->andReturn([8]); // Grant persists but the class was flipped back to public — it is already // in the public list, so it must not be appended a second time. $this->repository->shouldReceive('findById')->with(8)->andReturn($this->group(8, Offering::ACCESS_PUBLIC)); $data = $this->endpoint->index(new \WP_REST_Request())->get_data(); self::assertSame([], $data); } public function testIndexRespectsKindFilterForGrantedOfferings(): void { $this->repository->shouldReceive('findAll') ->with(0, Offering::KIND_PRIVATE_LESSON, Mockery::any(), Offering::ACCESS_PUBLIC) ->andReturn([]); $this->access->shouldReceive('findGrantedOfferingIds')->with(5)->andReturn([8]); // Granted class is a group class; the request filters to private lessons. $this->repository->shouldReceive('findById')->with(8)->andReturn($this->group(8, Offering::ACCESS_INVITE_ONLY)); $data = $this->endpoint->index(new \WP_REST_Request(['kind' => Offering::KIND_PRIVATE_LESSON]))->get_data(); self::assertSame([], $data); } public function testIndexIncludesInstructorNameForEachOffering(): void { $instructor = Mockery::mock(\WP_User::class); $instructor->first_name = 'Ada'; $instructor->last_name = 'Lovelace'; $instructor->nickname = 'ada_login'; $instructor->display_name = 'ada_login'; Functions\when('get_userdata')->justReturn($instructor); $this->repository->shouldReceive('findAll')->andReturn([$this->group(1, Offering::ACCESS_PUBLIC)]); $this->access->shouldReceive('findGrantedOfferingIds')->with(5)->andReturn([]); $data = $this->endpoint->index(new \WP_REST_Request())->get_data(); // Real name is shown, not the login-style display name. self::assertSame('Ada Lovelace', $data[0]['instructor_name']); } public function testIndexOmitsEtransferEmailFromPublicListing(): void { $this->repository->shouldReceive('findAll')->andReturn([ new Offering(instructorId: 3, kind: Offering::KIND_GROUP_CLASS, title: 'Choir', etransferEmail: 'studio@x.test', id: 1), ]); $this->access->shouldReceive('findGrantedOfferingIds')->with(5)->andReturn([]); $data = $this->endpoint->index(new \WP_REST_Request())->get_data(); self::assertArrayNotHasKey('etransfer_email', $data[0]); } public function testCanReadAllowsStudentsWhoMayBook(): void { Functions\when('is_user_logged_in')->justReturn(true); Functions\when('current_user_can')->alias( static fn (string $cap): bool => RoleManager::CAP_BOOK_LESSON === $cap ); self::assertTrue($this->endpoint->canRead()); } public function testCanReadAllowsOfferingManagersWhoCannotBook(): void { Functions\when('is_user_logged_in')->justReturn(true); Functions\when('current_user_can')->alias( static fn (string $cap): bool => RoleManager::CAP_MANAGE_OFFERINGS === $cap ); self::assertTrue($this->endpoint->canRead()); } public function testCanReadRejectsLoggedInUserWithNeitherCapability(): void { Functions\when('is_user_logged_in')->justReturn(true); Functions\when('current_user_can')->justReturn(false); self::assertFalse($this->endpoint->canRead()); } public function testCanReadRejectsLoggedOutVisitors(): void { Functions\when('is_user_logged_in')->justReturn(false); Functions\when('current_user_can')->justReturn(true); self::assertFalse($this->endpoint->canRead()); } public function testCreateRejectsTitleLongerThanColumnLimit(): void { Functions\when('sanitize_text_field')->returnArg(); Functions\when('sanitize_email')->returnArg(); $this->repository->shouldNotReceive('insert'); $request = new \WP_REST_Request([ 'kind' => Offering::KIND_GROUP_CLASS, 'title' => str_repeat('a', Offering::MAX_TITLE_LENGTH + 1), ]); $response = $this->endpoint->create($request); self::assertInstanceOf(\WP_Error::class, $response); self::assertSame(400, $response->error_data['invalid_offering']['status']); } public function testCreateRejectsScheduleNoteLongerThanColumnLimit(): void { Functions\when('sanitize_text_field')->returnArg(); Functions\when('sanitize_email')->returnArg(); $this->repository->shouldNotReceive('insert'); $request = new \WP_REST_Request([ 'kind' => Offering::KIND_GROUP_CLASS, 'title' => 'Choir', 'schedule_note' => str_repeat('a', Offering::MAX_SCHEDULE_NOTE_LENGTH + 1), ]); $response = $this->endpoint->create($request); self::assertInstanceOf(\WP_Error::class, $response); self::assertSame(400, $response->error_data['invalid_offering']['status']); } }