homeworkRepository = new InMemoryHomeworkRepository(); $this->attachmentRepository = new InMemoryHomeworkAttachmentRepository(); $this->fileStorage = new InMemoryFileStorage(); } #[Test] public function downloadReturnsStreamedResponseForExistingAttachment(): void { $homework = $this->createHomework(); $this->homeworkRepository->save($homework); $attachment = $this->createAttachment('exercices.pdf', 'homework/files/exercices.pdf'); $this->attachmentRepository->save($homework->id, $attachment); $this->fileStorage->upload('homework/files/exercices.pdf', 'PDF content here', 'application/pdf'); $controller = $this->createController(self::TEACHER_ID); $response = $controller->download((string) $homework->id, (string) $attachment->id); self::assertInstanceOf(StreamedResponse::class, $response); self::assertSame(200, $response->getStatusCode()); self::assertSame('application/pdf', $response->headers->get('Content-Type')); self::assertStringContainsString('exercices.pdf', $response->headers->get('Content-Disposition') ?? ''); } #[Test] public function downloadReturns404ForNonExistentAttachment(): void { $homework = $this->createHomework(); $this->homeworkRepository->save($homework); $controller = $this->createController(self::TEACHER_ID); $this->expectException(NotFoundHttpException::class); $controller->download((string) $homework->id, 'non-existent-attachment-id'); } #[Test] public function downloadReturns404WhenFileNotFoundInStorage(): void { $homework = $this->createHomework(); $this->homeworkRepository->save($homework); $attachment = $this->createAttachment('missing.pdf', 'homework/files/missing.pdf'); $this->attachmentRepository->save($homework->id, $attachment); // File NOT uploaded to storage — simulates a missing blob $controller = $this->createController(self::TEACHER_ID); $this->expectException(NotFoundHttpException::class); $controller->download((string) $homework->id, (string) $attachment->id); } #[Test] public function downloadDeniesAccessToNonOwnerTeacher(): void { $homework = $this->createHomework(); $this->homeworkRepository->save($homework); $attachment = $this->createAttachment('exercices.pdf', 'homework/files/exercices.pdf'); $this->attachmentRepository->save($homework->id, $attachment); $controller = $this->createController(self::OTHER_TEACHER_ID); $this->expectException(AccessDeniedHttpException::class); $controller->download((string) $homework->id, (string) $attachment->id); } #[Test] public function listDeniesAccessToNonOwnerTeacher(): void { $homework = $this->createHomework(); $this->homeworkRepository->save($homework); $controller = $this->createController(self::OTHER_TEACHER_ID); $this->expectException(AccessDeniedHttpException::class); $controller->list((string) $homework->id); } #[Test] public function deleteDeniesAccessToNonOwnerTeacher(): void { $homework = $this->createHomework(); $this->homeworkRepository->save($homework); $attachment = $this->createAttachment('exercices.pdf', 'homework/files/exercices.pdf'); $this->attachmentRepository->save($homework->id, $attachment); $controller = $this->createController(self::OTHER_TEACHER_ID); $this->expectException(AccessDeniedHttpException::class); $controller->delete((string) $homework->id, (string) $attachment->id); } #[Test] public function downloadDeniesAccessToUnauthenticatedUser(): void { $homework = $this->createHomework(); $this->homeworkRepository->save($homework); $controller = $this->createControllerWithoutUser(); $this->expectException(AccessDeniedHttpException::class); $controller->download((string) $homework->id, 'any-attachment-id'); } #[Test] public function listReturnsAttachmentsForOwner(): void { $homework = $this->createHomework(); $this->homeworkRepository->save($homework); $attachment = $this->createAttachment('exercices.pdf', 'homework/files/exercices.pdf'); $this->attachmentRepository->save($homework->id, $attachment); $controller = $this->createController(self::TEACHER_ID); $response = $controller->list((string) $homework->id); self::assertSame(200, $response->getStatusCode()); /** @var array $data */ $data = json_decode((string) $response->getContent(), true); self::assertCount(1, $data); self::assertSame('exercices.pdf', $data[0]['filename']); } #[Test] public function deleteRemovesAttachmentAndFile(): void { $homework = $this->createHomework(); $this->homeworkRepository->save($homework); $attachment = $this->createAttachment('exercices.pdf', 'homework/files/exercices.pdf'); $this->attachmentRepository->save($homework->id, $attachment); $this->fileStorage->upload('homework/files/exercices.pdf', 'content', 'application/pdf'); $controller = $this->createController(self::TEACHER_ID); $response = $controller->delete((string) $homework->id, (string) $attachment->id); self::assertSame(204, $response->getStatusCode()); self::assertEmpty($this->attachmentRepository->findByHomeworkId($homework->id)); self::assertFalse($this->fileStorage->has('homework/files/exercices.pdf')); } private function createHomework(): Homework { return Homework::creer( tenantId: TenantId::fromString(self::TENANT_ID), classId: ClassId::fromString('550e8400-e29b-41d4-a716-446655440020'), subjectId: SubjectId::fromString('550e8400-e29b-41d4-a716-446655440030'), teacherId: UserId::fromString(self::TEACHER_ID), title: 'Devoir test', description: 'Description', dueDate: new DateTimeImmutable('2026-05-01'), now: new DateTimeImmutable('2026-04-09'), ); } private function createAttachment(string $filename, string $filePath): HomeworkAttachment { return new HomeworkAttachment( id: HomeworkAttachmentId::generate(), filename: $filename, filePath: $filePath, fileSize: 5000, mimeType: 'application/pdf', uploadedAt: new DateTimeImmutable('2026-04-09'), ); } private function createController(string $teacherId): HomeworkAttachmentController { $securityUser = new SecurityUser( userId: UserId::fromString($teacherId), email: 'teacher@example.com', hashedPassword: 'hashed', tenantId: TenantId::fromString(self::TENANT_ID), roles: ['ROLE_PROF'], ); $security = $this->createMock(Security::class); $security->method('getUser')->willReturn($securityUser); $uploadHandler = $this->createUploadHandler($this->homeworkRepository, $this->fileStorage); return new HomeworkAttachmentController( security: $security, homeworkRepository: $this->homeworkRepository, attachmentRepository: $this->attachmentRepository, uploadHandler: $uploadHandler, fileStorage: $this->fileStorage, ); } private function createControllerWithoutUser(): HomeworkAttachmentController { $security = $this->createMock(Security::class); $security->method('getUser')->willReturn(null); $uploadHandler = $this->createUploadHandler($this->homeworkRepository, $this->fileStorage); return new HomeworkAttachmentController( security: $security, homeworkRepository: $this->homeworkRepository, attachmentRepository: $this->attachmentRepository, uploadHandler: $uploadHandler, fileStorage: $this->fileStorage, ); } private function createUploadHandler(HomeworkRepository $homeworkRepository, FileStorage $fileStorage): UploadHomeworkAttachmentHandler { $clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-04-09 10:00:00'); } }; return new UploadHomeworkAttachmentHandler($homeworkRepository, $fileStorage, $clock); } }