db = Mockery::mock(\wpdb::class); $this->db->prefix = 'wp_'; $this->repo = new OfferingRepository($this->db); } public function testInsertCallsWpdbInsertAndReturnsId(): void { Functions\expect('current_time')->with('mysql')->andReturn('2026-04-01 12:00:00'); $this->db->shouldReceive('insert') ->once() ->with( 'wp_us_offerings', Mockery::on(static function (array $data): bool { return $data['instructor_id'] === 5 && $data['kind'] === Offering::KIND_PRIVATE_LESSON && $data['title'] === '30-min Piano' && $data['price'] === 35.00 && $data['allow_weekly'] === 0 && $data['is_active'] === 1 && $data['created_at'] === '2026-04-01 12:00:00'; }), Mockery::type('array') ); $this->db->insert_id = 42; $offering = new Offering( instructorId: 5, kind: Offering::KIND_PRIVATE_LESSON, title: '30-min Piano', price: 35.00, durationMinutes: 30, ); self::assertSame(42, $this->repo->insert($offering)); } public function testUpdateReturnsTrueOnSuccess(): void { $this->db->shouldReceive('update') ->once() ->with( 'wp_us_offerings', Mockery::on(static fn (array $data): bool => $data['title'] === 'Renamed' && $data['allow_weekly'] === 1), ['id' => 7], Mockery::type('array'), ['%d'] ) ->andReturn(1); $offering = new Offering( instructorId: 5, kind: Offering::KIND_PRIVATE_LESSON, title: 'Renamed', allowWeekly: true, id: 7, ); self::assertTrue($this->repo->update(7, $offering)); } public function testUpdateReturnsFalseWhenWpdbReturnsFalse(): void { $this->db->shouldReceive('update')->once()->andReturn(false); $offering = new Offering(5, Offering::KIND_GROUP_CLASS, 'Choir', id: 9); self::assertFalse($this->repo->update(9, $offering)); } public function testFindByIdReturnsNullWhenNotFound(): void { $this->db->shouldReceive('prepare')->once()->andReturn('SELECT ...'); $this->db->shouldReceive('get_row')->once()->andReturn(null); self::assertNull($this->repo->findById(99)); } public function testFindByIdReturnsOfferingWhenFound(): void { $this->db->shouldReceive('prepare')->andReturn('SELECT ...'); $this->db->shouldReceive('get_row')->andReturn($this->sampleRow()); $offering = $this->repo->findById(10); self::assertInstanceOf(Offering::class, $offering); self::assertSame(10, $offering->id); self::assertSame(3, $offering->instructorId); } public function testFindAllWithNoFiltersPreparesTableOnly(): void { $this->db->shouldReceive('prepare') ->once() ->with(Mockery::pattern('/WHERE 1 = 1/'), ['wp_us_offerings']) ->andReturn('SELECT ...'); $this->db->shouldReceive('get_results') ->once() ->with('SELECT ...') ->andReturn([$this->sampleRow()]); $offerings = $this->repo->findAll(); self::assertCount(1, $offerings); self::assertInstanceOf(Offering::class, $offerings[0]); } public function testFindAllActiveOnlyPreparesQuery(): void { $this->db->shouldReceive('prepare') ->once() ->with(Mockery::pattern('/is_active = %d/'), Mockery::on(static fn (array $p): bool => $p === ['wp_us_offerings', 1])) ->andReturn('SELECT ...'); $this->db->shouldReceive('get_results')->andReturn([]); self::assertSame([], $this->repo->findAll(activeOnly: true)); } public function testFindAllWithInstructorAndKindFilters(): void { $this->db->shouldReceive('prepare') ->once() ->with( Mockery::pattern('/instructor_id = %d AND kind = %s/'), Mockery::on(static fn (array $p): bool => $p === ['wp_us_offerings', 3, Offering::KIND_GROUP_CLASS]) ) ->andReturn('SELECT ...'); $this->db->shouldReceive('get_results')->andReturn([]); $this->repo->findAll(3, Offering::KIND_GROUP_CLASS); } public function testDeleteCallsWpdbDelete(): void { $this->db->shouldReceive('delete') ->once() ->with('wp_us_offerings', ['id' => 4], ['%d']) ->andReturn(1); self::assertTrue($this->repo->delete(4)); } private function sampleRow(): object { return (object) [ 'id' => '10', 'instructor_id' => '3', 'kind' => Offering::KIND_PRIVATE_LESSON, 'title' => '30-min Piano', 'description' => null, 'duration_minutes' => '30', 'price' => '35.00', 'currency' => 'CAD', 'billing_mode' => Offering::BILLING_ONE_TIME, 'allow_weekly' => '0', 'capacity' => null, 'term_start' => null, 'term_end' => null, 'schedule_note' => null, 'etransfer_email' => null, 'is_active' => '1', ]; } }