repository = Mockery::mock(OfferingRepository::class); $this->controller = new OfferingController($this->repository); $_POST = []; $_GET = []; Functions\when('current_user_can')->justReturn(true); Functions\when('get_current_user_id')->justReturn(3); Functions\when('check_admin_referer')->justReturn(true); Functions\when('admin_url')->justReturn('admin.php?page=us-offerings'); Functions\when('add_query_arg')->alias( static fn ($key, $value, $url) => $url . '&' . $key . '=' . $value ); Functions\when('wp_unslash')->returnArg(); Functions\when('sanitize_text_field')->returnArg(); Functions\when('sanitize_textarea_field')->returnArg(); Functions\when('sanitize_email')->returnArg(); Functions\when('sanitize_key')->alias( static fn ($key) => strtolower((string) preg_replace('/[^a-zA-Z0-9_\-]/', '', (string) $key)) ); Functions\when('absint')->alias(static fn ($value) => abs((int) $value)); Functions\when('wp_nonce_field')->justReturn(''); Functions\when('submit_button')->alias(static function (string $text = ''): void { echo $text; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- test stub }); Functions\when('mysql2date')->alias( static fn (string $format, string $date) => date($format, (int) strtotime($date)) ); } public function testAddGroupClassWithWeeklyTermComputesEndDate(): void { $_POST = [ 'usc_action' => 'add', 'title' => 'Ballet Beginners', 'kind' => Offering::KIND_GROUP_CLASS, 'billing_mode' => Offering::BILLING_FULL_TERM, 'capacity' => '8', 'term_start' => '2026-09-08', 'term_recurrence' => 'weekly', 'term_sessions' => '10', ]; $this->repository->shouldReceive('insert')->once()->with(Mockery::on( static fn (Offering $o) => '2026-09-08' === $o->termStart && '2026-11-10' === $o->termEnd ))->andReturn(1); $this->repository->shouldReceive('findAll')->andReturn([]); $this->render(); } public function testAddInviteOnlyGroupClassStoresInviteOnlyAccess(): void { $_POST = [ 'usc_action' => 'add', 'title' => 'Private Choir', 'kind' => Offering::KIND_GROUP_CLASS, 'invite_only' => '1', ]; $this->repository->shouldReceive('insert')->once()->with(Mockery::on( static fn (Offering $o) => Offering::ACCESS_INVITE_ONLY === $o->accessMode ))->andReturn(1); $this->repository->shouldReceive('findAll')->andReturn([]); $this->render(); } public function testAddWithoutInviteOnlyDefaultsToPublicAccess(): void { $_POST = [ 'usc_action' => 'add', 'title' => 'Open Choir', 'kind' => Offering::KIND_GROUP_CLASS, ]; $this->repository->shouldReceive('insert')->once()->with(Mockery::on( static fn (Offering $o) => Offering::ACCESS_PUBLIC === $o->accessMode ))->andReturn(1); $this->repository->shouldReceive('findAll')->andReturn([]); $this->render(); } public function testAddOneOffGroupClassEndsOnItsStartDate(): void { $_POST = [ 'usc_action' => 'add', 'title' => 'Recital Workshop', 'kind' => Offering::KIND_GROUP_CLASS, 'term_start' => '2026-09-08', 'term_recurrence' => 'single', 'term_sessions' => '10', ]; $this->repository->shouldReceive('insert')->once()->with(Mockery::on( static fn (Offering $o) => '2026-09-08' === $o->termStart && '2026-09-08' === $o->termEnd ))->andReturn(1); $this->repository->shouldReceive('findAll')->andReturn([]); $this->render(); } public function testInvalidTermStartLeavesTermDatesNull(): void { $_POST = [ 'usc_action' => 'add', 'title' => 'Choir', 'kind' => Offering::KIND_GROUP_CLASS, 'term_start' => 'not-a-date', 'term_recurrence' => 'weekly', 'term_sessions' => '10', ]; $this->repository->shouldReceive('insert')->once()->with(Mockery::on( static fn (Offering $o) => null === $o->termStart && null === $o->termEnd ))->andReturn(1); $this->repository->shouldReceive('findAll')->andReturn([]); $this->render(); } public function testOfferingListShowsIdAndTermRange(): void { $offering = new Offering( instructorId: 3, kind: Offering::KIND_GROUP_CLASS, title: 'Ballet Beginners', termStart: '2026-09-08', termEnd: '2026-11-10', id: 42, ); $this->repository->shouldReceive('findAll')->andReturn([$offering]); $html = $this->render(); self::assertStringContainsString('