json; } } class StubBroadcastModel { public array $inserted = []; public function insert(array $data) { $this->inserted[] = $data; return 1; } } class StubUserModel { public function __construct(private readonly array $parents = []) { } public function select($columns) { return $this; } public function join(...$args) { return $this; } public function where(...$args) { return $this; } public function findAll(): array { return $this->parents; } } class TestableBroadcastEmailController extends BroadcastEmailController { public bool $validationResult = true; public array $validationErrors = []; public $validator; public function __construct() { // keep the parent constructor from running to avoid real models } public function setBroadcastModel(StubBroadcastModel $model): void { $this->broadcastModel = $model; } public function setUserModel(StubUserModel $model): void { $this->userModel = $model; } public function setValidationResult(bool $result, array $errors = []): void { $this->validationResult = $result; $this->validationErrors = $errors; } protected function validate($rules, array $messages = []): bool { $this->validator = new class($this->validationErrors) { public function __construct(private readonly array $errors) { } public function getErrors(): array { return $this->errors; } }; return $this->validationResult; } protected function respondSuccess($data = null, string $message = 'Success', int $code = ResponseInterface::HTTP_OK) { return ['status' => true, 'message' => $message, 'data' => $data, 'code' => $code]; } protected function respondValidationError(array $errors, string $message = 'Validation failed') { return ['status' => false, 'message' => $message, 'errors' => $errors, 'code' => ResponseInterface::HTTP_UNPROCESSABLE_ENTITY]; } } class BroadcastEmailControllerTest extends CIUnitTestCase { private TestableBroadcastEmailController $controller; private StubBroadcastModel $broadcastModel; protected function setUp(): void { parent::setUp(); $this->controller = new TestableBroadcastEmailController(); $this->broadcastModel = new StubBroadcastModel(); $this->controller->setBroadcastModel($this->broadcastModel); } public function testSendValidationFails(): void { $this->controller->setValidationResult(false, ['subject' => 'required']); $this->controller->setRequest(new FakeBroadcastRequest(['subject' => '', 'body' => '', 'recipients' => []])); $response = $this->controller->send(); $this->assertSame(ResponseInterface::HTTP_UNPROCESSABLE_ENTITY, $response['code']); $this->assertSame('Validation failed', $response['message']); $this->assertArrayHasKey('subject', $response['errors']); } public function testSendStoresSentAndFailedRecipients(): void { $requestData = [ 'subject' => 'Weekly Update', 'body' => 'Hello parents', 'recipients' => ['parent1@example.com', 'bad-email'], ]; $this->controller->setRequest(new FakeBroadcastRequest($requestData)); $response = $this->controller->send(); $this->assertSame(ResponseInterface::HTTP_OK, $response['code']); $this->assertSame('Broadcast email sent successfully', $response['message']); $this->assertSame(['parent1@example.com'], $response['data']['sent']); $this->assertSame(['bad-email'], $response['data']['failed']); $this->assertCount(1, $this->broadcastModel->inserted); $data = $this->broadcastModel->inserted[0]; $this->assertSame('Weekly Update', $data['subject']); $this->assertSame('Hello parents', $data['body']); $this->assertSame(1, $data['total_sent']); $this->assertArrayHasKey('created_at', $data); } public function testParentsReturnsParentList(): void { $parents = [ ['id' => 10, 'firstname' => 'Test', 'lastname' => 'Parent', 'email' => 'test@parent.com'], ]; $this->controller->setUserModel(new StubUserModel($parents)); $response = $this->controller->parents(); $this->assertSame(ResponseInterface::HTTP_OK, $response['code']); $this->assertSame('Parent list retrieved successfully', $response['message']); $this->assertSame($parents, $response['data']); } }