pref = Mockery::mock(InstructorNotificationPref::class); } private function mailer(): InstructorNotificationMailer { return new InstructorNotificationMailer($this->pref); } private function instructor(string $email): \WP_User { $user = Mockery::mock(\WP_User::class); $user->user_email = $email; return $user; } public function testLessonNoticeGoesToAnOptedInInstructor(): void { $this->pref->shouldReceive('wants')->with(9)->andReturn(true); Functions\when('get_userdata')->justReturn($this->instructor('teacher@studio.test')); Functions\expect('wp_mail') ->once() ->with( 'teacher@studio.test', Mockery::on(static fn (string $subject): bool => str_contains($subject, '30 min piano')), Mockery::on(static fn (string $body): bool => str_contains($body, 'Ada') && str_contains($body, 'Jul 1')) ) ->andReturn(true); self::assertTrue($this->mailer()->notifyLessonBooked(9, 'Ada', '30 min piano', 'Jul 1, 2026 10:00 AM')); } public function testWeeklyLessonNoticeCountsTheOccurrences(): void { $this->pref->shouldReceive('wants')->with(9)->andReturn(true); Functions\when('get_userdata')->justReturn($this->instructor('teacher@studio.test')); Functions\expect('wp_mail') ->once() ->with( 'teacher@studio.test', Mockery::type('string'), Mockery::on(static fn (string $body): bool => str_contains($body, '3 weekly lessons')) ) ->andReturn(true); self::assertTrue($this->mailer()->notifyLessonBooked(9, 'Ada', '30 min piano', 'Jul 1, 2026 10:00 AM', 3)); } public function testEnrollmentNoticeGoesToAnOptedInInstructor(): void { $this->pref->shouldReceive('wants')->with(9)->andReturn(true); Functions\when('get_userdata')->justReturn($this->instructor('teacher@studio.test')); Functions\expect('wp_mail') ->once() ->with( 'teacher@studio.test', Mockery::on(static fn (string $subject): bool => str_contains($subject, 'Choir')), Mockery::on(static fn (string $body): bool => str_contains($body, 'Ada') && str_contains($body, 'Choir')) ) ->andReturn(true); self::assertTrue($this->mailer()->notifyEnrollment(9, 'Ada', 'Choir')); } public function testSendsNothingWhenTheInstructorHasNotOptedIn(): void { $this->pref->shouldReceive('wants')->with(9)->andReturn(false); // Not even a user lookup: the preference is the first gate. Functions\expect('get_userdata')->never(); Functions\expect('wp_mail')->never(); self::assertFalse($this->mailer()->notifyLessonBooked(9, 'Ada', '30 min piano', 'Jul 1, 2026 10:00 AM')); self::assertFalse($this->mailer()->notifyEnrollment(9, 'Ada', 'Choir')); } public function testSendsNothingWhenTheInstructorAccountIsGone(): void { $this->pref->shouldReceive('wants')->with(9)->andReturn(true); Functions\when('get_userdata')->justReturn(false); Functions\expect('wp_mail')->never(); self::assertFalse($this->mailer()->notifyEnrollment(9, 'Ada', 'Choir')); } public function testSendsNothingWhenTheInstructorHasNoEmail(): void { $this->pref->shouldReceive('wants')->with(9)->andReturn(true); Functions\when('get_userdata')->justReturn($this->instructor('')); Functions\expect('wp_mail')->never(); self::assertFalse($this->mailer()->notifyLessonBooked(9, 'Ada', '30 min piano', 'Jul 1, 2026 10:00 AM')); } }