diff --git a/DailyGoals.drawio b/DailyGoals.drawio
index b0937cb..ca3101c 100644
--- a/DailyGoals.drawio
+++ b/DailyGoals.drawio
@@ -37,12 +37,6 @@
-
-
-
-
-
-
diff --git a/ai/backend_prompt_template.md b/ai/backend_prompt_template.md
index c16ef26..1aa846d 100644
--- a/ai/backend_prompt_template.md
+++ b/ai/backend_prompt_template.md
@@ -20,41 +20,29 @@ Code patterns to follow:
- Entities: constructor with properties, getters
- DTOs: simple data containers for creation
- Repositories: interfaces that define data access
- - Do not write unit tests for concrete repository implementations
- (e.g., Doctrine/persistence-backed). They are exercised by e2e
- tests. Use cases are tested with fake repositories.
- Use cases: business logic with Request objects
- When throwing exceptions, add @throws docblock
- Fakes: in-memory implementations for testing
- Look at tests/Fakes/ for examples
- Find/lookup methods must return a new instance of the entity, not the stored reference
- Tests: follow existing patterns in tests/Unit/[Entity]/UseCases/
- - In setUp, only use fake repositories for entities under test - construct dependency objects directly with `new` (e.g., `new Text(....)`) instead of creating them through their fake repositories
-- Lines should not exceed 80 columns, but should use up to 80 columns when possible - do not split lines unnecessarily
+ - In setUp, only use fake repositories for entities under test — construct dependency objects directly with `new` (e.g., `new Text(....)`) instead of creating them through their fake repositories
+- Lines should not exceed 80 columns, but should use up to 80 columns when possible — do not split lines unnecessarily
- Imports: always put use statements at the top of the file, never use inline imports (e.g., \App\Foo\Bar::class)
-- Variable names: use explicit, descriptive names - never single-letter or abbreviated variables (e.g., use $sponsorship not $s, $event not $e)
-- Never use em-dashes (—) in code, comments, commit messages, or any
- written output. Use a regular hyphen (-), a colon, or rephrase
- with parentheses instead.
+- Variable names: use explicit, descriptive names — never single-letter or abbreviated variables (e.g., use $sponsorship not $s, $event not $e)
Git commit style:
-- Subject: present tense, imperative mood (add, create, test, fix)
-- Subject: lowercase, short (3-6 words)
-- Match subject patterns found in git history
-- Add a body when the change needs explanation beyond the subject -
- e.g., why the change was made, non-obvious tradeoffs, or notable
- implementation details. Skip the body for trivial/self-evident commits.
-- Separate subject and body with a blank line; wrap body at ~72 columns
+- Present tense, imperative mood (add, create, test, fix)
+- Lowercase
+- Short (3-6 words)
+- Match patterns found in git history
Git commits:
- Tests should be committed first, before implementation
-- Group related changes together in a single commit (e.g., a new class
- plus its registration, or a getter plus the property it exposes).
- Avoid mixing unrelated concerns in one commit.
-- Keep commits small and focused - prefer many small commits over few
- large ones, but don't artificially split a single logical change
- across multiple commits
+- One commit per file - each new file gets its own commit
+- Make commits SMALL and FREQUENT - every meaningful change should be a commit
- Commits are for reviewing and documenting the development of code
+- A commit can be as simple as adding one import, one getter, one property, etc.
- Don't wait to commit - commit as you go
- Run `php-cs-fixer fix` on worked on directories before committing
diff --git a/ai/frontend_prompt_template.md b/ai/frontend_prompt_template.md
index a865577..24eb7c3 100644
--- a/ai/frontend_prompt_template.md
+++ b/ai/frontend_prompt_template.md
@@ -17,31 +17,22 @@ Code patterns to follow:
- First, explore the codebase to understand existing entity patterns
- Look at similar pages for reference
- Tests: follow existing patterns in cypress/e2e/
-- Lines should not exceed 80 columns, but should use up to 80 columns when possible - do not split lines unnecessarily
+- Lines should not exceed 80 columns, but should use up to 80 columns when possible — do not split lines unnecessarily
- Imports: always put imports at the top of the file
-- Variable names: use explicit, descriptive names - never single-letter or abbreviated variables (e.g., use sponsorship not s, event not e)
-- Never use em-dashes (—) in code, comments, commit messages, or any
- written output. Use a regular hyphen (-), a colon, or rephrase
- with parentheses instead.
+- Variable names: use explicit, descriptive names — never single-letter or abbreviated variables (e.g., use sponsorship not s, event not e)
Git commit style:
-- Subject: present tense, imperative mood (add, create, test, fix)
-- Subject: lowercase, short (3-6 words)
-- Match subject patterns found in git history
-- Add a body when the change needs explanation beyond the subject -
- e.g., why the change was made, non-obvious tradeoffs, or notable
- implementation details. Skip the body for trivial/self-evident commits.
-- Separate subject and body with a blank line; wrap body at ~72 columns
+- Present tense, imperative mood (add, create, test, fix)
+- Lowercase
+- Short (3-6 words)
+- Match patterns found in git history
Git commits:
- Tests should be committed first, before implementation
-- Group related changes together in a single commit (e.g., a new class
- plus its registration, or a getter plus the property it exposes).
- Avoid mixing unrelated concerns in one commit.
-- Keep commits small and focused - prefer many small commits over few
- large ones, but don't artificially split a single logical change
- across multiple commits
+- One commit per file - each new file gets its own commit
+- Make commits SMALL and FREQUENT - every meaningful change should be a commit
- Commits are for reviewing and documenting the development of code
+- A commit can be as simple as adding one import, one getter, one property, etc.
- Don't wait to commit - commit as you go
Branch naming:
diff --git a/app/Auth/AdminMiddleware.php b/app/Auth/AdminMiddleware.php
deleted file mode 100644
index 47e16be..0000000
--- a/app/Auth/AdminMiddleware.php
+++ /dev/null
@@ -1,64 +0,0 @@
-getAttribute('user');
-
- if (!$user instanceof User || !$user->isAdmin()) {
- return $this->forbidden($request);
- }
-
- return $handler->handle($request);
- }
-
- private function forbidden(
- ServerRequestInterface $request
- ): ResponseInterface {
- $response = new Response(403);
-
- if ($this->wantsJson($request)) {
- $response->getBody()->write(
- json_encode(['error' => 'forbidden'])
- );
- return $response->withHeader(
- 'Content-Type',
- 'application/json'
- );
- }
-
- $html = file_get_contents(
- __DIR__ . '/../../views/templates/forbidden.php'
- );
- $response->getBody()->write($html);
-
- return $response->withHeader('Content-Type', 'text/html');
- }
-
- private function wantsJson(ServerRequestInterface $request): bool
- {
- $path = $request->getUri()->getPath();
- if (str_starts_with($path, '/api/')) {
- return true;
- }
-
- $accept = $request->getHeaderLine('Accept');
- if (str_contains($accept, 'application/json')) {
- return true;
- }
-
- return false;
- }
-}
diff --git a/app/Auth/AuthController.php b/app/Auth/AuthController.php
deleted file mode 100644
index 0864730..0000000
--- a/app/Auth/AuthController.php
+++ /dev/null
@@ -1,166 +0,0 @@
-parseBody($request);
-
- try {
- $user = $authenticateUser->execute(
- new AuthenticateUserRequest(
- email: $data['email'] ?? null,
- password: $data['password'] ?? null,
- )
- );
- } catch (BadRequestException $exception) {
- return $this->errorResponse(
- $response,
- 400,
- $exception->getMessage()
- );
- } catch (UnauthorizedException $exception) {
- return $this->errorResponse(
- $response,
- 401,
- $exception->getMessage()
- );
- }
-
- $session = $createSession->execute($user);
-
- return $this->userResponse($response, $user)
- ->withHeader(
- 'Set-Cookie',
- $this->buildSetCookie($session->getToken())
- );
- }
-
- public function register(
- Request $request,
- Response $response,
- CreateUser $createUser,
- CreateSession $createSession,
- ): Response {
- $data = $this->parseBody($request);
-
- try {
- $user = $createUser->execute(new CreateUserRequest(
- email: $data['email'] ?? null,
- password: $data['password'] ?? null,
- isAdmin: false,
- ));
- } catch (BadRequestException $exception) {
- return $this->errorResponse(
- $response,
- 400,
- $exception->getMessage()
- );
- }
-
- $session = $createSession->execute($user);
-
- return $this->userResponse($response, $user)
- ->withHeader(
- 'Set-Cookie',
- $this->buildSetCookie($session->getToken())
- );
- }
-
- public function logout(
- Request $request,
- Response $response,
- SessionRepository $sessionRepo,
- ): Response {
- $cookies = $request->getCookieParams();
- $token = $cookies[AuthMiddleware::COOKIE_NAME] ?? null;
-
- if ($token !== null) {
- $sessionRepo->deleteByToken($token);
- }
-
- return $response->withStatus(204)
- ->withHeader('Set-Cookie', $this->buildClearCookie());
- }
-
- public function me(Request $request, Response $response): Response
- {
- $user = $request->getAttribute('user');
- if (!$user instanceof User) {
- return $this->errorResponse(
- $response,
- 401,
- 'unauthenticated'
- );
- }
-
- return $this->userResponse($response, $user);
- }
-
- private function parseBody(Request $request): array
- {
- return json_decode((string) $request->getBody(), true) ?? [];
- }
-
- private function userResponse(Response $response, User $user): Response
- {
- $response->getBody()->write(json_encode([
- 'user' => [
- 'id' => $user->getId(),
- 'email' => $user->getEmail()->value(),
- 'isAdmin' => $user->isAdmin(),
- ],
- ]));
-
- return $response->withHeader(
- 'Content-Type',
- 'application/json'
- );
- }
-
- private function errorResponse(
- Response $response,
- int $status,
- string $message,
- ): Response {
- $response->getBody()->write(
- json_encode(['error' => $message])
- );
-
- return $response->withStatus($status)
- ->withHeader('Content-Type', 'application/json');
- }
-
- private function buildSetCookie(string $token): string
- {
- $maxAge = self::COOKIE_MAX_AGE;
-
- return AuthMiddleware::COOKIE_NAME . '=' . $token
- . '; Path=/; HttpOnly; SameSite=Lax; Max-Age=' . $maxAge;
- }
-
- private function buildClearCookie(): string
- {
- return AuthMiddleware::COOKIE_NAME . '=;'
- . ' Path=/; HttpOnly; SameSite=Lax; Max-Age=0';
- }
-}
diff --git a/app/Auth/AuthMiddleware.php b/app/Auth/AuthMiddleware.php
deleted file mode 100644
index e72fba8..0000000
--- a/app/Auth/AuthMiddleware.php
+++ /dev/null
@@ -1,84 +0,0 @@
-getCookieParams();
- $token = $cookies[self::COOKIE_NAME] ?? null;
-
- if ($token === null) {
- return $this->unauthorized($request);
- }
-
- $session = $this->sessionRepo->findByToken($token);
- if ($session === null) {
- return $this->unauthorized($request);
- }
-
- if ($session->isExpired($this->clock->now())) {
- $this->sessionRepo->deleteByToken($token);
- return $this->unauthorized($request);
- }
-
- $user = $this->userRepo->find($session->getUserId());
- if ($user === null) {
- return $this->unauthorized($request);
- }
-
- return $handler->handle(
- $request->withAttribute('user', $user)
- );
- }
-
- private function unauthorized(
- ServerRequestInterface $request
- ): ResponseInterface {
- if ($this->wantsJson($request)) {
- $response = new Response(401);
- $response->getBody()->write(
- json_encode(['error' => 'unauthenticated'])
- );
- return $response->withHeader(
- 'Content-Type',
- 'application/json'
- );
- }
-
- return new Response(302)->withHeader('Location', '/login');
- }
-
- private function wantsJson(ServerRequestInterface $request): bool
- {
- $path = $request->getUri()->getPath();
- if (str_starts_with($path, '/api/')) {
- return true;
- }
-
- $accept = $request->getHeaderLine('Accept');
- if (str_contains($accept, 'application/json')) {
- return true;
- }
-
- return false;
- }
-}
diff --git a/app/Auth/BcryptPasswordHasher.php b/app/Auth/BcryptPasswordHasher.php
deleted file mode 100644
index 8593710..0000000
--- a/app/Auth/BcryptPasswordHasher.php
+++ /dev/null
@@ -1,16 +0,0 @@
-filePath = __DIR__ . '/../../data/sessions.json';
- }
-
- public function create(CreateSessionDto $dto): Session
- {
- $sessions = $this->readSessions();
-
- $sessions[] = [
- 'token' => $dto->token,
- 'userId' => $dto->userId,
- 'createdAt' => $dto->createdAt->format(DATE_ATOM),
- 'expiresAt' => $dto->expiresAt->format(DATE_ATOM),
- ];
- $this->writeSessions($sessions);
-
- return new Session(
- token: $dto->token,
- userId: $dto->userId,
- createdAt: $dto->createdAt,
- expiresAt: $dto->expiresAt,
- );
- }
-
- public function findByToken(string $token): ?Session
- {
- $sessions = $this->readSessions();
-
- foreach ($sessions as $data) {
- if ($data['token'] === $token) {
- return new Session(
- token: $data['token'],
- userId: $data['userId'],
- createdAt: new DateTimeImmutable($data['createdAt']),
- expiresAt: new DateTimeImmutable($data['expiresAt']),
- );
- }
- }
-
- return null;
- }
-
- public function deleteByToken(string $token): void
- {
- $sessions = $this->readSessions();
- $filtered = array_values(array_filter(
- $sessions,
- function (array $data) use ($token) {
- return $data['token'] !== $token;
- }
- ));
- $this->writeSessions($filtered);
- }
-
- private function readSessions(): array
- {
- if (!file_exists($this->filePath)) {
- return [];
- }
-
- $content = file_get_contents($this->filePath);
-
- return json_decode($content, true) ?? [];
- }
-
- private function writeSessions(array $sessions): void
- {
- file_put_contents(
- $this->filePath,
- json_encode($sessions, JSON_PRETTY_PRINT)
- );
- }
-}
diff --git a/app/Auth/PasswordHasher.php b/app/Auth/PasswordHasher.php
deleted file mode 100644
index 332d105..0000000
--- a/app/Auth/PasswordHasher.php
+++ /dev/null
@@ -1,10 +0,0 @@
-token;
- }
-
- public function getUserId(): int
- {
- return $this->userId;
- }
-
- public function getCreatedAt(): DateTimeImmutable
- {
- return $this->createdAt;
- }
-
- public function getExpiresAt(): DateTimeImmutable
- {
- return $this->expiresAt;
- }
-
- public function isExpired(DateTimeImmutable $now): bool
- {
- return $now >= $this->expiresAt;
- }
-}
diff --git a/app/Auth/SessionRepository.php b/app/Auth/SessionRepository.php
deleted file mode 100644
index 073a16c..0000000
--- a/app/Auth/SessionRepository.php
+++ /dev/null
@@ -1,10 +0,0 @@
-clock->now();
- $expiresAt = $now->modify(self::SESSION_LIFETIME);
-
- return $this->sessionRepo->create(new CreateSessionDto(
- token: $this->tokenGenerator->generate(),
- userId: $user->getId(),
- createdAt: $now,
- expiresAt: $expiresAt,
- ));
- }
-}
diff --git a/app/Exceptions/ForbiddenException.php b/app/Exceptions/ForbiddenException.php
deleted file mode 100644
index c9b05cd..0000000
--- a/app/Exceptions/ForbiddenException.php
+++ /dev/null
@@ -1,5 +0,0 @@
-getAttribute('user');
- if (!$user instanceof User) {
- $response->getBody()->write(
- json_encode(['error' => 'unauthenticated'])
- );
- return $response->withStatus(401)
- ->withHeader('Content-Type', 'application/json');
- }
-
$data = json_decode((string) $request->getBody(), true) ?? [];
+ $userId = isset($data['userId']) ? (int) $data['userId'] : null;
$textId = isset($data['textId']) ? (int) $data['textId'] : null;
$name = $data['name'] ?? null;
$dateStart = $data['dateStart'] ?? null;
@@ -35,7 +26,7 @@ class PlanController
try {
$plan = $createPlanUseCase->execute(new CreatePlanRequest(
- userId: $user->getId(),
+ userId: $userId,
textId: $textId,
name: $name,
dateStart: $dateStart,
diff --git a/app/User/JsonUserRepository.php b/app/User/JsonUserRepository.php
index d5de3a2..bde8c62 100644
--- a/app/User/JsonUserRepository.php
+++ b/app/User/JsonUserRepository.php
@@ -21,17 +21,13 @@ class JsonUserRepository implements UserRepository
$users[] = [
'id' => $id,
- 'email' => $dto->email->value(),
- 'passwordHash' => $dto->passwordHash,
- 'isAdmin' => $dto->isAdmin,
+ 'email' => (string) $dto->email,
];
$this->writeUsers($users);
return new User(
id: $id,
email: $dto->email,
- passwordHash: $dto->passwordHash,
- isAdmin: $dto->isAdmin,
);
}
@@ -41,36 +37,16 @@ class JsonUserRepository implements UserRepository
foreach ($users as $data) {
if ($data['id'] === $id) {
- return $this->hydrate($data);
+ return new User(
+ id: $data['id'],
+ email: new EmailAddress($data['email']),
+ );
}
}
return null;
}
- public function findByEmail(EmailAddress $email): ?User
- {
- $users = $this->readUsers();
-
- foreach ($users as $data) {
- if ($data['email'] === $email->value()) {
- return $this->hydrate($data);
- }
- }
-
- return null;
- }
-
- private function hydrate(array $data): User
- {
- return new User(
- id: $data['id'],
- email: new EmailAddress($data['email']),
- passwordHash: $data['passwordHash'] ?? '',
- isAdmin: $data['isAdmin'] ?? false,
- );
- }
-
private function readUsers(): array
{
if (!file_exists($this->filePath)) {
diff --git a/app/User/UseCases/AuthenticateUser.php b/app/User/UseCases/AuthenticateUser.php
deleted file mode 100644
index a889a06..0000000
--- a/app/User/UseCases/AuthenticateUser.php
+++ /dev/null
@@ -1,50 +0,0 @@
-email === null) {
- throw new BadRequestException('email is required');
- }
-
- if ($request->password === null) {
- throw new BadRequestException('password is required');
- }
-
- $user = $this->userRepo->findByEmail(
- new EmailAddress($request->email)
- );
- if ($user === null) {
- throw new UnauthorizedException('invalid credentials');
- }
-
- $passwordMatches = $this->passwordHasher->verify(
- $request->password,
- $user->getPasswordHash()
- );
- if (!$passwordMatches) {
- throw new UnauthorizedException('invalid credentials');
- }
-
- return $user;
- }
-}
diff --git a/app/User/UseCases/AuthenticateUserRequest.php b/app/User/UseCases/AuthenticateUserRequest.php
deleted file mode 100644
index 953ff6e..0000000
--- a/app/User/UseCases/AuthenticateUserRequest.php
+++ /dev/null
@@ -1,11 +0,0 @@
-email === null) {
throw new BadRequestException('email is required');
}
- if ($dto->password === null) {
- throw new BadRequestException('password is required');
- }
-
- if (strlen($dto->password) < 8) {
- throw new BadRequestException(
- 'password must be at least 8 characters'
- );
- }
-
- $email = new EmailAddress($dto->email);
- if ($this->userRepo->findByEmail($email) !== null) {
- throw new BadRequestException('email already taken');
- }
-
- return $this->userRepo->create(new CreateUserDto(
- email: $email,
- passwordHash: $this->passwordHasher->hash($dto->password),
- isAdmin: $dto->isAdmin,
+ $this->userRepo->create(new CreateUserDto(
+ email: new EmailAddress($dto->email),
));
}
}
diff --git a/app/User/UseCases/CreateUserDto.php b/app/User/UseCases/CreateUserDto.php
index a9c38fa..e978287 100644
--- a/app/User/UseCases/CreateUserDto.php
+++ b/app/User/UseCases/CreateUserDto.php
@@ -8,7 +8,5 @@ class CreateUserDto
{
public function __construct(
public EmailAddress $email,
- public string $passwordHash,
- public bool $isAdmin = false,
) {}
}
diff --git a/app/User/UseCases/CreateUserRequest.php b/app/User/UseCases/CreateUserRequest.php
index c70ef72..7c48913 100644
--- a/app/User/UseCases/CreateUserRequest.php
+++ b/app/User/UseCases/CreateUserRequest.php
@@ -6,7 +6,5 @@ class CreateUserRequest
{
public function __construct(
public ?string $email,
- public ?string $password,
- public bool $isAdmin,
) {}
}
diff --git a/app/User/User.php b/app/User/User.php
index 9e8ae97..e685a32 100644
--- a/app/User/User.php
+++ b/app/User/User.php
@@ -9,8 +9,6 @@ class User
public function __construct(
private int $id,
private EmailAddress $email,
- private string $passwordHash,
- private bool $isAdmin,
) {}
public function getId(): int
@@ -22,14 +20,4 @@ class User
{
return $this->email;
}
-
- public function getPasswordHash(): string
- {
- return $this->passwordHash;
- }
-
- public function isAdmin(): bool
- {
- return $this->isAdmin;
- }
}
diff --git a/app/User/UserRepository.php b/app/User/UserRepository.php
index 278cd98..c58a649 100644
--- a/app/User/UserRepository.php
+++ b/app/User/UserRepository.php
@@ -3,11 +3,9 @@
namespace App\User;
use App\User\UseCases\CreateUserDto;
-use App\ValueObjects\EmailAddress;
interface UserRepository
{
public function create(CreateUserDto $dto): User;
public function find(int $id): ?User;
- public function findByEmail(EmailAddress $email): ?User;
}
diff --git a/app/ValueObjects/EmailAddress.php b/app/ValueObjects/EmailAddress.php
index e646f4c..6952ae9 100644
--- a/app/ValueObjects/EmailAddress.php
+++ b/app/ValueObjects/EmailAddress.php
@@ -11,11 +11,6 @@ class EmailAddress
$this->normalized = $email;
}
- public function value(): string
- {
- return $this->normalized;
- }
-
public function __toString(): string
{
return $this->normalized;
diff --git a/app/View/ViewController.php b/app/View/ViewController.php
index 4fd8d84..c131683 100644
--- a/app/View/ViewController.php
+++ b/app/View/ViewController.php
@@ -37,24 +37,4 @@ class ViewController
return $response;
}
-
- public function login(Response $response): Response
- {
- $html = file_get_contents(
- __DIR__ . '/../../views/templates/login.php'
- );
- $response->getBody()->write($html);
-
- return $response;
- }
-
- public function register(Response $response): Response
- {
- $html = file_get_contents(
- __DIR__ . '/../../views/templates/register.php'
- );
- $response->getBody()->write($html);
-
- return $response;
- }
}
diff --git a/bootstrap/app.php b/bootstrap/app.php
index c05dc0f..1ceeb8e 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -3,10 +3,6 @@
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use DI\Bridge\Slim\Bridge;
-use Slim\Routing\RouteCollectorProxy;
-use App\Auth\AdminMiddleware;
-use App\Auth\AuthController;
-use App\Auth\AuthMiddleware;
use App\View\ViewController;
use App\Text\TextController;
use App\Node\NodeController;
@@ -18,48 +14,19 @@ $app = Bridge::create($container);
// change first param to false for production
$app->addErrorMiddleware(true, true, true);
-// Public routes (no auth required)
-$app->get('/login', [ViewController::class, 'login']);
-$app->get('/register', [ViewController::class, 'register']);
-$app->post('/api/auth/login', [AuthController::class, 'login']);
-$app->post('/api/auth/register', [AuthController::class, 'register']);
+$app->get('/home', [ViewController::class, 'home']);
+$app->get('/admin', [ViewController::class, 'admin']);
+$app->get('/admin/texts', [ViewController::class, 'texts']);
+$app->get('/admin/texts/{textId}', [ViewController::class, 'text']);
-// Authenticated routes (any logged-in user)
-$app->group('', function (RouteCollectorProxy $group) {
- $group->get('/home', [ViewController::class, 'home']);
+$app->get('/api/texts', [TextController::class, 'getTexts']);
+$app->get('/api/texts/{textId}', [TextController::class, 'getText']);
+$app->post('/api/texts', [TextController::class, 'createText']);
- $group->post('/api/auth/logout', [AuthController::class, 'logout']);
- $group->get('/api/auth/me', [AuthController::class, 'me']);
+$app->get('/api/nodes/{textId}', [NodeController::class, 'getNodesOfText']);
+$app->post('/api/nodes/bulk', [NodeController::class, 'bulkCreateNodes']);
+$app->post('/api/nodes', [NodeController::class, 'createNode']);
- $group->get('/api/texts', [TextController::class, 'getTexts']);
- $group->get(
- '/api/texts/{textId}',
- [TextController::class, 'getText']
- );
-
- $group->get(
- '/api/nodes/{textId}',
- [NodeController::class, 'getNodesOfText']
- );
-
- $group->post('/api/plans', [PlanController::class, 'createPlan']);
-})->add(AuthMiddleware::class);
-
-// Admin-only routes
-$app->group('', function (RouteCollectorProxy $group) {
- $group->get('/admin', [ViewController::class, 'admin']);
- $group->get('/admin/texts', [ViewController::class, 'texts']);
- $group->get(
- '/admin/texts/{textId}',
- [ViewController::class, 'text']
- );
-
- $group->post('/api/texts', [TextController::class, 'createText']);
- $group->post(
- '/api/nodes/bulk',
- [NodeController::class, 'bulkCreateNodes']
- );
- $group->post('/api/nodes', [NodeController::class, 'createNode']);
-})->add(AdminMiddleware::class)->add(AuthMiddleware::class);
+$app->post('/api/plans', [PlanController::class, 'createPlan']);
return $app;
diff --git a/bootstrap/container.php b/bootstrap/container.php
index 2ae6e4c..fb80996 100644
--- a/bootstrap/container.php
+++ b/bootstrap/container.php
@@ -2,14 +2,6 @@
use DI;
use DI\Container;
-use App\Auth\BcryptPasswordHasher;
-use App\Auth\Clock;
-use App\Auth\JsonSessionRepository;
-use App\Auth\PasswordHasher;
-use App\Auth\RandomTokenGenerator;
-use App\Auth\SessionRepository;
-use App\Auth\SystemClock;
-use App\Auth\TokenGenerator;
use App\Text\TextRepository;
use App\Text\JsonTextRepository;
use App\Node\NodeRepository;
@@ -28,11 +20,6 @@ $container = new Container([
UserRepository::class => DI\autowire(JsonUserRepository::class),
ScheduledNodeRepository::class =>
DI\autowire(JsonScheduledNodeRepository::class),
- SessionRepository::class =>
- DI\autowire(JsonSessionRepository::class),
- TokenGenerator::class => DI\autowire(RandomTokenGenerator::class),
- Clock::class => DI\autowire(SystemClock::class),
- PasswordHasher::class => DI\autowire(BcryptPasswordHasher::class),
]);
return $container;
diff --git a/cypress/e2e/admin.cy.js b/cypress/e2e/admin.cy.js
index 3bd205f..2c8054c 100644
--- a/cypress/e2e/admin.cy.js
+++ b/cypress/e2e/admin.cy.js
@@ -1,7 +1,6 @@
describe('The admin page', () => {
beforeEach(() => {
cy.exec('npm run db:seed')
- cy.loginAsAdmin()
cy.visit('/admin')
})
afterEach(() => {
diff --git a/cypress/e2e/adminText.cy.js b/cypress/e2e/adminText.cy.js
index f5cad89..84be4cd 100644
--- a/cypress/e2e/adminText.cy.js
+++ b/cypress/e2e/adminText.cy.js
@@ -1,7 +1,6 @@
describe('The admin text detail page', () => {
beforeEach(() => {
cy.exec('npm run db:seed')
- cy.loginAsAdmin()
cy.intercept('GET', '/api/texts/0').as('getText')
cy.intercept('GET', '/api/nodes/0').as('getNodes')
cy.visit('/admin/texts/0')
diff --git a/cypress/e2e/adminTextBulkAdd.cy.js b/cypress/e2e/adminTextBulkAdd.cy.js
index 7d1afc2..03159a2 100644
--- a/cypress/e2e/adminTextBulkAdd.cy.js
+++ b/cypress/e2e/adminTextBulkAdd.cy.js
@@ -1,7 +1,6 @@
describe('Bulk add children on the admin text detail page', () => {
beforeEach(() => {
cy.exec('npm run db:seed')
- cy.loginAsAdmin()
cy.intercept('GET', '/api/texts/0').as('getText')
cy.intercept('GET', '/api/nodes/0').as('getNodes')
cy.visit('/admin/texts/0')
diff --git a/cypress/e2e/adminTextToggle.cy.js b/cypress/e2e/adminTextToggle.cy.js
index 23cf830..c75132b 100644
--- a/cypress/e2e/adminTextToggle.cy.js
+++ b/cypress/e2e/adminTextToggle.cy.js
@@ -1,7 +1,6 @@
describe('Toggle display of child nodes', () => {
beforeEach(() => {
cy.exec('npm run db:seed')
- cy.loginAsAdmin()
cy.intercept('GET', '/api/texts/0').as('getText')
cy.intercept('GET', '/api/nodes/0').as('getNodes')
cy.visit('/admin/texts/0')
diff --git a/cypress/e2e/auth.cy.js b/cypress/e2e/auth.cy.js
deleted file mode 100644
index ae22d22..0000000
--- a/cypress/e2e/auth.cy.js
+++ /dev/null
@@ -1,87 +0,0 @@
-describe('Authentication flows', () => {
- beforeEach(() => {
- cy.exec('npm run db:seed')
- })
-
- afterEach(() => {
- cy.exec('npm run db:wipe')
- })
-
- it('unauthenticated home redirects to login', () => {
- cy.visit('/home')
- cy.url().should('include', '/login')
- })
-
- it('login form submits and redirects to home', () => {
- cy.visit('/login')
- cy.get('#email').type('user@example.com')
- cy.get('#password').type('password1')
- cy.get('#login-form').submit()
- cy.url().should('include', '/home')
- cy.get('h1').should('contain', 'Home')
- })
-
- it('login shows error on wrong password', () => {
- cy.visit('/login')
- cy.get('#email').type('user@example.com')
- cy.get('#password').type('wrongpassword')
- cy.get('#login-form').submit()
- cy.get('#login-error').should('be.visible')
- cy.url().should('include', '/login')
- })
-
- it('register creates user and redirects to home', () => {
- cy.visit('/register')
- cy.get('#email').type('fresh@example.com')
- cy.get('#password').type('password1')
- cy.get('#register-form').submit()
- cy.url().should('include', '/home')
- })
-
- it('register shows error on short password', () => {
- cy.visit('/register')
- cy.get('#email').type('another@example.com')
- cy.get('#password').invoke(
- 'removeAttr',
- 'minlength'
- )
- cy.get('#password').type('short')
- cy.get('#register-form').submit()
- cy.get('#register-error').should('be.visible')
- cy.url().should('include', '/register')
- })
-
- it('register shows error on duplicate email', () => {
- cy.visit('/register')
- cy.get('#email').type('user@example.com')
- cy.get('#password').type('password1')
- cy.get('#register-form').submit()
- cy.get('#register-error').should('be.visible')
- cy.url().should('include', '/register')
- })
-
- it('logout clears session and redirects to login', () => {
- cy.loginAsUser()
- cy.visit('/home')
- cy.get('#logout').click()
- cy.url().should('include', '/login')
- cy.visit('/home')
- cy.url().should('include', '/login')
- })
-
- it('non-admin user hitting /admin gets 403', () => {
- cy.loginAsUser()
- cy.request({
- url: '/admin',
- failOnStatusCode: false,
- }).then((response) => {
- expect(response.status).to.eq(403)
- })
- })
-
- it('admin user can access /admin', () => {
- cy.loginAsAdmin()
- cy.visit('/admin')
- cy.get('#texts').should('exist')
- })
-})
diff --git a/cypress/e2e/home.cy.js b/cypress/e2e/home.cy.js
index 83c6984..d0ba1fe 100644
--- a/cypress/e2e/home.cy.js
+++ b/cypress/e2e/home.cy.js
@@ -1,7 +1,6 @@
describe('The home page', () => {
beforeEach(() => {
cy.exec('npm run db:seed')
- cy.loginAsUser()
})
afterEach(() => {
cy.exec('npm run db:wipe')
diff --git a/cypress/e2e/homeCreatePlan.cy.js b/cypress/e2e/homeCreatePlan.cy.js
index 8509a61..3c9ee87 100644
--- a/cypress/e2e/homeCreatePlan.cy.js
+++ b/cypress/e2e/homeCreatePlan.cy.js
@@ -1,7 +1,6 @@
describe('Create plan modal on the home page', () => {
beforeEach(() => {
cy.exec('npm run db:seed')
- cy.loginAsUser()
cy.intercept('GET', '/api/texts').as('getTexts')
cy.visit('/home')
cy.wait('@getTexts')
@@ -61,6 +60,7 @@ describe('Create plan modal on the home page', () => {
cy.wait('@createPlan').then((createPlanRequest) => {
expect(createPlanRequest.response.statusCode).to.eq(201)
expect(createPlanRequest.request.body).to.deep.equal({
+ userId: 0,
textId: 0,
name: 'My reading plan',
dateStart: '2025-01-01',
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 61e6549..66ea16e 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -1,15 +1,25 @@
-Cypress.Commands.add('login', (email, password) => {
- cy.request({
- method: 'POST',
- url: '/api/auth/login',
- body: { email, password },
- })
-})
-
-Cypress.Commands.add('loginAsAdmin', () => {
- cy.login('admin@example.com', 'admin1234')
-})
-
-Cypress.Commands.add('loginAsUser', () => {
- cy.login('user@example.com', 'password1')
-})
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add('login', (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
\ No newline at end of file
diff --git a/data/seedDb.php b/data/seedDb.php
index aaa329f..922ca90 100644
--- a/data/seedDb.php
+++ b/data/seedDb.php
@@ -28,27 +28,15 @@ $nodes = [
],
];
-// Default credentials:
-// admin@example.com / admin1234 (admin)
-// user@example.com / password1 (regular user)
$users = [
[
'id' => 0,
- 'email' => 'admin@example.com',
- 'passwordHash' => password_hash('admin1234', PASSWORD_DEFAULT),
- 'isAdmin' => true,
- ],
- [
- 'id' => 1,
'email' => 'user@example.com',
- 'passwordHash' => password_hash('password1', PASSWORD_DEFAULT),
- 'isAdmin' => false,
],
];
$plans = [];
$scheduledNodes = [];
-$sessions = [];
$fileDataMap = [
'texts.json' => $texts,
@@ -56,7 +44,6 @@ $fileDataMap = [
'users.json' => $users,
'plans.json' => $plans,
'scheduledNodes.json' => $scheduledNodes,
- 'sessions.json' => $sessions,
];
foreach ($fileDataMap as $file => $data) {
diff --git a/data/wipeDb.php b/data/wipeDb.php
index 6428a92..658bb2f 100644
--- a/data/wipeDb.php
+++ b/data/wipeDb.php
@@ -6,7 +6,6 @@ $files = [
'users.json',
'plans.json',
'scheduledNodes.json',
- 'sessions.json',
];
foreach ($files as $file) {
diff --git a/public/js/auth.js b/public/js/auth.js
deleted file mode 100644
index 0e8fc96..0000000
--- a/public/js/auth.js
+++ /dev/null
@@ -1,75 +0,0 @@
-async function submitAuthForm(endpoint, email, password, errorElement) {
- errorElement.hidden = true;
- errorElement.textContent = '';
-
- const response = await fetch(endpoint, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'same-origin',
- body: JSON.stringify({ email, password }),
- });
-
- if (response.ok) {
- window.location.href = '/home';
- return;
- }
-
- let message = 'Something went wrong';
- try {
- const body = await response.json();
- if (body.error) {
- message = body.error;
- }
- } catch (parseError) {
- // fall through to generic message
- }
- errorElement.textContent = message;
- errorElement.hidden = false;
-}
-
-async function logout() {
- await fetch('/api/auth/logout', {
- method: 'POST',
- credentials: 'same-origin',
- });
- window.location.href = '/login';
-}
-
-document.addEventListener('DOMContentLoaded', () => {
- const loginForm = document.getElementById('login-form');
- if (loginForm !== null) {
- const errorElement = document.getElementById('login-error');
- loginForm.addEventListener('submit', async (submitEvent) => {
- submitEvent.preventDefault();
- const email = document.getElementById('email').value;
- const password = document.getElementById('password').value;
- await submitAuthForm(
- '/api/auth/login',
- email,
- password,
- errorElement,
- );
- });
- }
-
- const registerForm = document.getElementById('register-form');
- if (registerForm !== null) {
- const errorElement = document.getElementById('register-error');
- registerForm.addEventListener('submit', async (submitEvent) => {
- submitEvent.preventDefault();
- const email = document.getElementById('email').value;
- const password = document.getElementById('password').value;
- await submitAuthForm(
- '/api/auth/register',
- email,
- password,
- errorElement,
- );
- });
- }
-
- const logoutButton = document.getElementById('logout');
- if (logoutButton !== null) {
- logoutButton.addEventListener('click', logout);
- }
-});
diff --git a/public/js/home.js b/public/js/home.js
index e7abc7c..b46a051 100644
--- a/public/js/home.js
+++ b/public/js/home.js
@@ -3,9 +3,7 @@ document.addEventListener('DOMContentLoaded', () => {
const createPlanModal = document.getElementById('create-plan-modal');
async function loadTexts() {
- const response = await fetch('/api/texts', {
- credentials: 'same-origin',
- });
+ const response = await fetch('/api/texts');
const texts = await response.json();
textsList.innerHTML = texts
.map(text =>
@@ -69,8 +67,8 @@ document.addEventListener('DOMContentLoaded', () => {
const response = await fetch('/api/plans', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- credentials: 'same-origin',
body: JSON.stringify({
+ userId: 0,
textId: textId,
name: planName,
dateStart: dateStart,
diff --git a/public/js/text.js b/public/js/text.js
index 59fc4ee..0388bf6 100644
--- a/public/js/text.js
+++ b/public/js/text.js
@@ -1,7 +1,7 @@
document.addEventListener('DOMContentLoaded', () => {
const textId = window.location.pathname.split('/').pop();
- fetch('/api/texts/' + textId, { credentials: 'same-origin' })
+ fetch('/api/texts/' + textId)
.then(res => res.json())
.then(text => {
const h1 = document.createElement('h1');
@@ -13,7 +13,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
function fetchAndRenderNodes(textId) {
- return fetch('/api/nodes/' + textId, { credentials: 'same-origin' })
+ return fetch('/api/nodes/' + textId)
.then(res => res.json())
.then(nodes => {
const existing = document.querySelector('#text-detail > ul');
@@ -113,7 +113,6 @@ function toggleAddForm(li, parentNodeId, textId) {
fetch('/api/nodes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- credentials: 'same-origin',
body: JSON.stringify({ textId: parseInt(textId), title, parentNodeId }),
})
.then(res => {
@@ -158,7 +157,6 @@ function toggleBulkAddForm(li, parentNodeId, textId) {
fetch('/api/nodes/bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- credentials: 'same-origin',
body: JSON.stringify({ textId: parseInt(textId), parentNodeId, titlePrefix, count }),
})
.then(res => {
diff --git a/public/js/texts.js b/public/js/texts.js
index 029196d..1937749 100644
--- a/public/js/texts.js
+++ b/public/js/texts.js
@@ -3,9 +3,7 @@ document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('texts-form');
async function loadTexts() {
- const res = await fetch('/api/texts', {
- credentials: 'same-origin',
- });
+ const res = await fetch('/api/texts');
const texts = await res.json();
textsList.innerHTML = texts.map(text =>
'
{
const formData = new FormData(form);
const res = await fetch('/api/texts', {
method: 'POST',
- credentials: 'same-origin',
body: formData,
});
if (res.ok) {
diff --git a/tests/Fakes/FakeClock.php b/tests/Fakes/FakeClock.php
deleted file mode 100644
index 31e649b..0000000
--- a/tests/Fakes/FakeClock.php
+++ /dev/null
@@ -1,36 +0,0 @@
-assertUtc($currentTime);
- }
-
- public function now(): DateTimeImmutable
- {
- return $this->currentTime;
- }
-
- public function setTime(DateTimeImmutable $newTime): void
- {
- $this->assertUtc($newTime);
- $this->currentTime = $newTime;
- }
-
- private function assertUtc(DateTimeImmutable $time): void
- {
- if ($time->getTimezone()->getOffset($time) !== 0) {
- throw new InvalidArgumentException(
- 'FakeClock requires a DateTimeImmutable in UTC.'
- );
- }
- }
-}
diff --git a/tests/Fakes/FakePasswordHasher.php b/tests/Fakes/FakePasswordHasher.php
deleted file mode 100644
index 4e9a17d..0000000
--- a/tests/Fakes/FakePasswordHasher.php
+++ /dev/null
@@ -1,20 +0,0 @@
-token,
- userId: $dto->userId,
- createdAt: $dto->createdAt,
- expiresAt: $dto->expiresAt,
- );
- $this->existingSessions[$dto->token] = $session;
-
- return $session;
- }
-
- public function findByToken(string $token): ?Session
- {
- $session = $this->existingSessions[$token] ?? null;
- if ($session === null) {
- return null;
- }
-
- return new Session(
- token: $session->getToken(),
- userId: $session->getUserId(),
- createdAt: $session->getCreatedAt(),
- expiresAt: $session->getExpiresAt(),
- );
- }
-
- public function deleteByToken(string $token): void
- {
- unset($this->existingSessions[$token]);
- }
-}
diff --git a/tests/Fakes/FakeTokenGenerator.php b/tests/Fakes/FakeTokenGenerator.php
deleted file mode 100644
index fbe8913..0000000
--- a/tests/Fakes/FakeTokenGenerator.php
+++ /dev/null
@@ -1,25 +0,0 @@
-callCount % count($this->predefinedTokens);
- $this->callCount++;
-
- return $this->predefinedTokens[$index];
- }
-}
diff --git a/tests/Fakes/FakeUserRepository.php b/tests/Fakes/FakeUserRepository.php
index b2e766a..062cae8 100644
--- a/tests/Fakes/FakeUserRepository.php
+++ b/tests/Fakes/FakeUserRepository.php
@@ -5,7 +5,6 @@ namespace Tests\Fakes;
use App\User\UseCases\CreateUserDto;
use App\User\User;
use App\User\UserRepository;
-use App\ValueObjects\EmailAddress;
class FakeUserRepository implements UserRepository
{
@@ -29,28 +28,6 @@ class FakeUserRepository implements UserRepository
return new User(
id: $user->getId(),
email: $user->getEmail(),
- passwordHash: $user->getPasswordHash(),
- isAdmin: $user->isAdmin(),
- );
- }
-
- public function findByEmail(EmailAddress $email): ?User
- {
- $user = array_find(
- $this->existingUsers,
- function (User $user) use ($email) {
- return $user->getEmail()->value() === $email->value();
- }
- );
- if ($user === null) {
- return null;
- }
-
- return new User(
- id: $user->getId(),
- email: $user->getEmail(),
- passwordHash: $user->getPasswordHash(),
- isAdmin: $user->isAdmin(),
);
}
@@ -60,8 +37,6 @@ class FakeUserRepository implements UserRepository
$user = new User(
id: $id,
email: $dto->email,
- passwordHash: $dto->passwordHash,
- isAdmin: $dto->isAdmin,
);
$this->existingUsers[$id] = $user;
diff --git a/tests/Unit/Auth/Middleware/AdminMiddlewareTest.php b/tests/Unit/Auth/Middleware/AdminMiddlewareTest.php
deleted file mode 100644
index 54eecd3..0000000
--- a/tests/Unit/Auth/Middleware/AdminMiddlewareTest.php
+++ /dev/null
@@ -1,118 +0,0 @@
-middleware = new AdminMiddleware();
- }
-
- private function makeApiRequest(?User $user): ServerRequestInterface
- {
- $request = new ServerRequestFactory()
- ->createServerRequest('POST', 'http://localhost/api/texts');
- if ($user !== null) {
- $request = $request->withAttribute('user', $user);
- }
- return $request;
- }
-
- private function makeHtmlRequest(?User $user): ServerRequestInterface
- {
- $request = new ServerRequestFactory()
- ->createServerRequest('GET', 'http://localhost/admin')
- ->withHeader('Accept', 'text/html');
- if ($user !== null) {
- $request = $request->withAttribute('user', $user);
- }
- return $request;
- }
-
- private function makeHandler(): RequestHandlerInterface
- {
- return new class implements RequestHandlerInterface {
- public bool $wasCalled = false;
-
- public function handle(
- ServerRequestInterface $request
- ): \Psr\Http\Message\ResponseInterface {
- $this->wasCalled = true;
- return new Response(200);
- }
- };
- }
-
- private function makeUser(bool $isAdmin): User
- {
- return new User(
- id: 1,
- email: new EmailAddress('test@test.com'),
- passwordHash: '',
- isAdmin: $isAdmin,
- );
- }
-
- public function test_passes_through_when_user_is_admin(): void
- {
- $handler = $this->makeHandler();
-
- $response = $this->middleware->process(
- $this->makeApiRequest($this->makeUser(isAdmin: true)),
- $handler,
- );
-
- $this->assertTrue($handler->wasCalled);
- $this->assertEquals(200, $response->getStatusCode());
- }
-
- public function test_returns_403_json_when_user_not_admin_for_api(): void
- {
- $response = $this->middleware->process(
- $this->makeApiRequest($this->makeUser(isAdmin: false)),
- $this->makeHandler(),
- );
-
- $this->assertEquals(403, $response->getStatusCode());
- $this->assertStringContainsString(
- 'application/json',
- $response->getHeaderLine('Content-Type')
- );
- }
-
- public function test_returns_403_html_when_user_not_admin_for_view(): void
- {
- $response = $this->middleware->process(
- $this->makeHtmlRequest($this->makeUser(isAdmin: false)),
- $this->makeHandler(),
- );
-
- $this->assertEquals(403, $response->getStatusCode());
- $this->assertStringContainsString(
- '403 Forbidden',
- (string) $response->getBody()
- );
- }
-
- public function test_returns_403_when_no_user_attribute(): void
- {
- $response = $this->middleware->process(
- $this->makeApiRequest(null),
- $this->makeHandler(),
- );
-
- $this->assertEquals(403, $response->getStatusCode());
- }
-}
diff --git a/tests/Unit/Auth/Middleware/AuthMiddlewareTest.php b/tests/Unit/Auth/Middleware/AuthMiddlewareTest.php
deleted file mode 100644
index 8408384..0000000
--- a/tests/Unit/Auth/Middleware/AuthMiddlewareTest.php
+++ /dev/null
@@ -1,161 +0,0 @@
-userRepo = new FakeUserRepository();
- $this->sessionRepo = new FakeSessionRepository();
- $this->clock = new FakeClock(
- new DateTimeImmutable('2025-01-01T12:00:00+00:00')
- );
- $this->user = $this->userRepo->create(new CreateUserDto(
- email: new EmailAddress('test@test.com'),
- passwordHash: '',
- ));
- $this->middleware = new AuthMiddleware(
- $this->sessionRepo,
- $this->userRepo,
- $this->clock,
- );
- }
-
- private function makeApiRequest(
- ?string $cookieToken = null
- ): ServerRequestInterface {
- $request = new ServerRequestFactory()
- ->createServerRequest('GET', 'http://localhost/api/texts');
- if ($cookieToken !== null) {
- $request = $request->withCookieParams([
- 'auth_token' => $cookieToken,
- ]);
- }
- return $request;
- }
-
- private function makeHtmlRequest(
- ?string $cookieToken = null
- ): ServerRequestInterface {
- $request = new ServerRequestFactory()
- ->createServerRequest('GET', 'http://localhost/home')
- ->withHeader('Accept', 'text/html');
- if ($cookieToken !== null) {
- $request = $request->withCookieParams([
- 'auth_token' => $cookieToken,
- ]);
- }
- return $request;
- }
-
- private function makeHandler(): RequestHandlerInterface
- {
- return new class implements RequestHandlerInterface {
- public ?ServerRequestInterface $capturedRequest = null;
-
- public function handle(
- ServerRequestInterface $request
- ): \Psr\Http\Message\ResponseInterface {
- $this->capturedRequest = $request;
- return new Response(200);
- }
- };
- }
-
- public function test_returns_401_json_when_cookie_missing(): void
- {
- $response = $this->middleware->process(
- $this->makeApiRequest(),
- $this->makeHandler(),
- );
-
- $this->assertEquals(401, $response->getStatusCode());
- $this->assertStringContainsString(
- 'application/json',
- $response->getHeaderLine('Content-Type')
- );
- }
-
- public function test_returns_401_when_token_not_in_repo(): void
- {
- $response = $this->middleware->process(
- $this->makeApiRequest('unknown-token'),
- $this->makeHandler(),
- );
-
- $this->assertEquals(401, $response->getStatusCode());
- }
-
- public function test_returns_401_when_token_expired(): void
- {
- $this->sessionRepo->create(new CreateSessionDto(
- token: 'expired-token',
- userId: $this->user->getId(),
- createdAt: new DateTimeImmutable('2024-12-01T00:00:00+00:00'),
- expiresAt: new DateTimeImmutable('2024-12-08T00:00:00+00:00'),
- ));
-
- $response = $this->middleware->process(
- $this->makeApiRequest('expired-token'),
- $this->makeHandler(),
- );
-
- $this->assertEquals(401, $response->getStatusCode());
- }
-
- public function test_attaches_user_to_request_on_success(): void
- {
- $this->sessionRepo->create(new CreateSessionDto(
- token: 'valid-token',
- userId: $this->user->getId(),
- createdAt: new DateTimeImmutable('2025-01-01T00:00:00+00:00'),
- expiresAt: new DateTimeImmutable('2025-01-08T00:00:00+00:00'),
- ));
- $handler = $this->makeHandler();
-
- $this->middleware->process(
- $this->makeApiRequest('valid-token'),
- $handler,
- );
-
- $attached = $handler->capturedRequest->getAttribute('user');
- $this->assertInstanceOf(User::class, $attached);
- $this->assertEquals(
- 'test@test.com',
- $attached->getEmail()->value()
- );
- }
-
- public function test_redirects_to_login_when_html_unauthenticated(): void
- {
- $response = $this->middleware->process(
- $this->makeHtmlRequest(),
- $this->makeHandler(),
- );
-
- $this->assertEquals(302, $response->getStatusCode());
- $this->assertEquals('/login', $response->getHeaderLine('Location'));
- }
-}
diff --git a/tests/Unit/Auth/UseCases/CreateSessionTest.php b/tests/Unit/Auth/UseCases/CreateSessionTest.php
deleted file mode 100644
index a79eb9c..0000000
--- a/tests/Unit/Auth/UseCases/CreateSessionTest.php
+++ /dev/null
@@ -1,85 +0,0 @@
-sessionRepo = new FakeSessionRepository();
- $this->tokenGenerator = new FakeTokenGenerator(
- ['generated-token-abc']
- );
- $this->clock = new FakeClock(
- new DateTimeImmutable('2025-01-01T12:00:00+00:00')
- );
- $this->useCase = new CreateSession(
- $this->sessionRepo,
- $this->tokenGenerator,
- $this->clock,
- );
- $this->user = new User(
- id: 7,
- email: new EmailAddress('test@test.com'),
- passwordHash: 'hashed:password1',
- isAdmin: false,
- );
- }
-
- public function test_creates_session_for_user(): void
- {
- $session = $this->useCase->execute($this->user);
-
- $this->assertEquals(7, $session->getUserId());
- }
-
- public function test_session_token_comes_from_generator(): void
- {
- $session = $this->useCase->execute($this->user);
-
- $this->assertEquals('generated-token-abc', $session->getToken());
- }
-
- public function test_session_created_at_is_now(): void
- {
- $session = $this->useCase->execute($this->user);
-
- $this->assertEquals(
- new DateTimeImmutable('2025-01-01T12:00:00+00:00'),
- $session->getCreatedAt()
- );
- }
-
- public function test_session_expires_in_seven_days(): void
- {
- $session = $this->useCase->execute($this->user);
-
- $this->assertEquals(
- new DateTimeImmutable('2025-01-08T12:00:00+00:00'),
- $session->getExpiresAt()
- );
- }
-
- public function test_session_is_persisted(): void
- {
- $this->useCase->execute($this->user);
-
- $found = $this->sessionRepo->findByToken('generated-token-abc');
- $this->assertNotNull($found);
- }
-}
diff --git a/tests/Unit/Plan/UseCases/CreatePlanTest.php b/tests/Unit/Plan/UseCases/CreatePlanTest.php
index f88bc89..667e72c 100644
--- a/tests/Unit/Plan/UseCases/CreatePlanTest.php
+++ b/tests/Unit/Plan/UseCases/CreatePlanTest.php
@@ -39,7 +39,6 @@ class CreatePlanTest extends TestCase
$this->scheduledNodeRepo = new FakeScheduledNodeRepository();
$this->userRepo->create(new CreateUserDto(
email: new EmailAddress('test@test.com'),
- passwordHash: '',
));
$this->createScheduledNode = new CreateScheduledNode(
scheduledNodeRepo: $this->scheduledNodeRepo,
@@ -233,8 +232,8 @@ class CreatePlanTest extends TestCase
));
}
- public function test_scheduled_nodes_are_scheduled_on_different_days(): void
- {
+ public function test_scheduled_nodes_are_scheduled_on_different_days(): void
+ {
$text = $this->textRepo->find(0);
$rootNode = $this->nodeRepo->create(new CreateNodeDto(
text: $text,
@@ -270,10 +269,10 @@ class CreatePlanTest extends TestCase
new DateTimeImmutable('2025-01-02'),
$childTwo->getDate()
);
- }
+ }
- public function test_more_scheduled_nodes_than_days(): void
- {
+ public function test_more_scheduled_nodes_than_days(): void
+ {
$text = $this->textRepo->find(0);
$rootNode = $this->nodeRepo->create(new CreateNodeDto(
text: $text,
@@ -320,5 +319,5 @@ class CreatePlanTest extends TestCase
new DateTimeImmutable('2025-01-02'),
$childThree->getDate()
);
- }
+ }
}
diff --git a/tests/Unit/ScheduledNode/UseCases/CreateScheduledNodeTest.php b/tests/Unit/ScheduledNode/UseCases/CreateScheduledNodeTest.php
index 477a07a..d2bfc2b 100644
--- a/tests/Unit/ScheduledNode/UseCases/CreateScheduledNodeTest.php
+++ b/tests/Unit/ScheduledNode/UseCases/CreateScheduledNodeTest.php
@@ -30,12 +30,7 @@ class CreateScheduledNodeTest extends TestCase
$this->planRepo = new FakePlanRepository();
$this->planRepo->create(new CreatePlanDto(
name: 'testplan',
- user: new User(
- id: 0,
- email: new EmailAddress('test@test.com'),
- passwordHash: 'hashed:password1',
- isAdmin: false,
- ),
+ user: new User(0, new EmailAddress('test@test.com')),
));
$this->useCase = new CreateScheduledNode(
$this->scheduledNodeRepo,
diff --git a/tests/Unit/User/UseCases/AuthenticateUserTest.php b/tests/Unit/User/UseCases/AuthenticateUserTest.php
deleted file mode 100644
index 88bb1a9..0000000
--- a/tests/Unit/User/UseCases/AuthenticateUserTest.php
+++ /dev/null
@@ -1,95 +0,0 @@
-userRepo = new FakeUserRepository();
- $this->passwordHasher = new FakePasswordHasher();
- $createUser = new CreateUser(
- $this->userRepo,
- $this->passwordHasher,
- );
- $createUser->execute(new CreateUserRequest(
- email: 'test@test.com',
- password: 'password1',
- isAdmin: false,
- ));
- $this->useCase = new AuthenticateUser(
- $this->userRepo,
- $this->passwordHasher,
- );
- }
-
- public function test_returns_user_on_valid_credentials(): void
- {
- $user = $this->useCase->execute(new AuthenticateUserRequest(
- email: 'test@test.com',
- password: 'password1',
- ));
-
- $this->assertInstanceOf(User::class, $user);
- $this->assertEquals('test@test.com', $user->getEmail()->value());
- }
-
- public function test_throws_bad_request_when_email_null(): void
- {
- $this->expectException(BadRequestException::class);
- $this->expectExceptionMessage('email is required');
-
- $this->useCase->execute(new AuthenticateUserRequest(
- email: null,
- password: 'password1',
- ));
- }
-
- public function test_throws_bad_request_when_password_null(): void
- {
- $this->expectException(BadRequestException::class);
- $this->expectExceptionMessage('password is required');
-
- $this->useCase->execute(new AuthenticateUserRequest(
- email: 'test@test.com',
- password: null,
- ));
- }
-
- public function test_throws_unauthorized_on_wrong_password(): void
- {
- $this->expectException(UnauthorizedException::class);
- $this->expectExceptionMessage('invalid credentials');
-
- $this->useCase->execute(new AuthenticateUserRequest(
- email: 'test@test.com',
- password: 'wrongpassword',
- ));
- }
-
- public function test_throws_unauthorized_when_email_not_found(): void
- {
- $this->expectException(UnauthorizedException::class);
- $this->expectExceptionMessage('invalid credentials');
-
- $this->useCase->execute(new AuthenticateUserRequest(
- email: 'missing@test.com',
- password: 'password1',
- ));
- }
-}
diff --git a/tests/Unit/User/UseCases/CreateUserTest.php b/tests/Unit/User/UseCases/CreateUserTest.php
index 0333a52..180046f 100644
--- a/tests/Unit/User/UseCases/CreateUserTest.php
+++ b/tests/Unit/User/UseCases/CreateUserTest.php
@@ -6,119 +6,33 @@ use App\Exceptions\BadRequestException;
use App\User\User;
use App\User\UseCases\CreateUser;
use App\User\UseCases\CreateUserRequest;
-use Tests\Fakes\FakePasswordHasher;
use Tests\Fakes\FakeUserRepository;
use PHPUnit\Framework\TestCase;
class CreateUserTest extends TestCase
{
- private FakeUserRepository $userRepo;
- private FakePasswordHasher $passwordHasher;
- private CreateUser $useCase;
-
- public function setUp(): void
- {
- $this->userRepo = new FakeUserRepository();
- $this->passwordHasher = new FakePasswordHasher();
- $this->useCase = new CreateUser(
- $this->userRepo,
- $this->passwordHasher,
- );
- }
-
public function test_create_user(): void
{
- $this->useCase->execute(new CreateUserRequest(
+ $userRepo = new FakeUserRepository();
+ $useCase = new CreateUser($userRepo);
+ $useCase->execute(new CreateUserRequest(
email: 'test@test.com',
- password: 'password1',
- isAdmin: false,
));
- $user = $this->userRepo->find(0);
+ $user = $userRepo->find(0);
$this->assertInstanceOf(User::class, $user);
$this->assertEquals('test@test.com', $user->getEmail());
}
public function test_throws_if_email_is_null(): void
{
+ $userRepo = new FakeUserRepository();
+ $useCase = new CreateUser($userRepo);
+
$this->expectException(BadRequestException::class);
$this->expectExceptionMessage('email is required');
- $this->useCase->execute(new CreateUserRequest(
+ $useCase->execute(new CreateUserRequest(
email: null,
- password: 'password1',
- isAdmin: false,
));
}
-
- public function test_is_admin_can_be_set_true(): void
- {
- $this->useCase->execute(new CreateUserRequest(
- email: 'test@test.com',
- password: 'password1',
- isAdmin: true,
- ));
- $user = $this->userRepo->find(0);
- $this->assertTrue($user->isAdmin());
- }
-
- public function test_throws_when_email_already_taken(): void
- {
- $this->useCase->execute(new CreateUserRequest(
- email: 'test@test.com',
- password: 'password1',
- isAdmin: false,
- ));
-
- $this->expectException(BadRequestException::class);
- $this->expectExceptionMessage('email already taken');
-
- $this->useCase->execute(new CreateUserRequest(
- email: 'test@test.com',
- password: 'password1',
- isAdmin: false
- ));
- }
-
- public function test_throws_if_password_is_null(): void
- {
- $this->expectException(BadRequestException::class);
- $this->expectExceptionMessage('password is required');
-
- $this->useCase->execute(new CreateUserRequest(
- email: 'test@test.com',
- password: null,
- isAdmin: false,
- ));
- }
-
- public function test_throws_if_password_too_short(): void
- {
- $this->expectException(BadRequestException::class);
- $this->expectExceptionMessage(
- 'password must be at least 8 characters'
- );
-
- $this->useCase->execute(new CreateUserRequest(
- email: 'test@test.com',
- password: 'short',
- isAdmin: false,
- ));
- }
-
- public function test_stores_hashed_password(): void
- {
- $this->useCase->execute(new CreateUserRequest(
- email: 'test@test.com',
- password: 'password1',
- isAdmin: false,
- ));
- $user = $this->userRepo->find(0);
- $this->assertNotEquals('password1', $user->getPasswordHash());
- $this->assertTrue(
- $this->passwordHasher->verify(
- 'password1',
- $user->getPasswordHash()
- )
- );
- }
}
diff --git a/tests/e2e/Controllers/AuthControllerTest.php b/tests/e2e/Controllers/AuthControllerTest.php
deleted file mode 100644
index 50e39a5..0000000
--- a/tests/e2e/Controllers/AuthControllerTest.php
+++ /dev/null
@@ -1,294 +0,0 @@
-userRepo = new FakeUserRepository();
- $this->sessionRepo = new FakeSessionRepository();
- $this->tokenGenerator = new FakeTokenGenerator(
- ['session-token-xyz']
- );
- $this->clock = new FakeClock(
- new DateTimeImmutable('2025-01-01T12:00:00+00:00')
- );
- $this->passwordHasher = new FakePasswordHasher();
-
- $this->createUser = new CreateUser(
- $this->userRepo,
- $this->passwordHasher,
- );
- $this->authenticateUser = new AuthenticateUser(
- $this->userRepo,
- $this->passwordHasher,
- );
- $this->createSession = new CreateSession(
- $this->sessionRepo,
- $this->tokenGenerator,
- $this->clock,
- );
-
- $this->createUser->execute(new CreateUserRequest(
- email: 'existing@test.com',
- password: 'password1',
- isAdmin: false,
- ));
-
- $this->controller = new AuthController();
- }
-
- private function makeJsonRequest(
- string $method,
- string $path,
- array $data,
- ): ServerRequestInterface {
- $body = new StreamFactory()->createStream(json_encode($data));
- return new ServerRequestFactory()
- ->createServerRequest($method, 'http://localhost' . $path)
- ->withHeader('Content-Type', 'application/json')
- ->withBody($body);
- }
-
- public function test_login_returns_200_and_user(): void
- {
- $response = $this->controller->login(
- $this->makeJsonRequest('POST', '/api/auth/login', [
- 'email' => 'existing@test.com',
- 'password' => 'password1',
- ]),
- new Response(),
- $this->authenticateUser,
- $this->createSession,
- );
-
- $this->assertEquals(200, $response->getStatusCode());
- $body = json_decode($response->getBody(), true);
- $this->assertEquals(
- 'existing@test.com',
- $body['user']['email']
- );
- }
-
- public function test_login_sets_auth_cookie(): void
- {
- $response = $this->controller->login(
- $this->makeJsonRequest('POST', '/api/auth/login', [
- 'email' => 'existing@test.com',
- 'password' => 'password1',
- ]),
- new Response(),
- $this->authenticateUser,
- $this->createSession,
- );
-
- $setCookie = $response->getHeaderLine('Set-Cookie');
- $this->assertStringContainsString(
- 'auth_token=session-token-xyz',
- $setCookie
- );
- $this->assertStringContainsString('HttpOnly', $setCookie);
- $this->assertStringContainsString('SameSite=Lax', $setCookie);
- $this->assertStringContainsString('Path=/', $setCookie);
- }
-
- public function test_login_creates_session(): void
- {
- $this->controller->login(
- $this->makeJsonRequest('POST', '/api/auth/login', [
- 'email' => 'existing@test.com',
- 'password' => 'password1',
- ]),
- new Response(),
- $this->authenticateUser,
- $this->createSession,
- );
-
- $this->assertNotNull(
- $this->sessionRepo->findByToken('session-token-xyz')
- );
- }
-
- public function test_login_returns_401_on_wrong_password(): void
- {
- $response = $this->controller->login(
- $this->makeJsonRequest('POST', '/api/auth/login', [
- 'email' => 'existing@test.com',
- 'password' => 'wrongpassword',
- ]),
- new Response(),
- $this->authenticateUser,
- $this->createSession,
- );
-
- $this->assertEquals(401, $response->getStatusCode());
- }
-
- public function test_login_returns_400_when_email_missing(): void
- {
- $response = $this->controller->login(
- $this->makeJsonRequest('POST', '/api/auth/login', [
- 'password' => 'password1',
- ]),
- new Response(),
- $this->authenticateUser,
- $this->createSession,
- );
-
- $this->assertEquals(400, $response->getStatusCode());
- }
-
- public function test_register_creates_user_and_logs_in(): void
- {
- $response = $this->controller->register(
- $this->makeJsonRequest('POST', '/api/auth/register', [
- 'email' => 'new@test.com',
- 'password' => 'password1',
- ]),
- new Response(),
- $this->createUser,
- $this->createSession,
- );
-
- $this->assertEquals(200, $response->getStatusCode());
- $body = json_decode($response->getBody(), true);
- $this->assertEquals('new@test.com', $body['user']['email']);
- $setCookie = $response->getHeaderLine('Set-Cookie');
- $this->assertStringContainsString(
- 'auth_token=session-token-xyz',
- $setCookie
- );
- }
-
- public function test_register_returns_400_on_short_password(): void
- {
- $response = $this->controller->register(
- $this->makeJsonRequest('POST', '/api/auth/register', [
- 'email' => 'new@test.com',
- 'password' => 'short',
- ]),
- new Response(),
- $this->createUser,
- $this->createSession,
- );
-
- $this->assertEquals(400, $response->getStatusCode());
- }
-
- public function test_register_returns_400_on_duplicate_email(): void
- {
- $response = $this->controller->register(
- $this->makeJsonRequest('POST', '/api/auth/register', [
- 'email' => 'existing@test.com',
- 'password' => 'password1',
- ]),
- new Response(),
- $this->createUser,
- $this->createSession,
- );
-
- $this->assertEquals(400, $response->getStatusCode());
- }
-
- public function test_register_ignores_is_admin_in_body(): void
- {
- $this->controller->register(
- $this->makeJsonRequest('POST', '/api/auth/register', [
- 'email' => 'sneaky@test.com',
- 'password' => 'password1',
- 'isAdmin' => true,
- ]),
- new Response(),
- $this->createUser,
- $this->createSession,
- );
-
- $newUser = $this->userRepo->findByEmail(
- new EmailAddress('sneaky@test.com')
- );
- $this->assertFalse($newUser->isAdmin());
- }
-
- public function test_logout_deletes_session_and_clears_cookie(): void
- {
- $this->sessionRepo->create(new CreateSessionDto(
- token: 'existing-session',
- userId: 0,
- createdAt: new DateTimeImmutable('2025-01-01T00:00:00+00:00'),
- expiresAt: new DateTimeImmutable('2025-01-08T00:00:00+00:00'),
- ));
-
- $request = $this->makeJsonRequest(
- 'POST',
- '/api/auth/logout',
- []
- )->withCookieParams(['auth_token' => 'existing-session']);
-
- $response = $this->controller->logout(
- $request,
- new Response(),
- $this->sessionRepo,
- );
-
- $this->assertEquals(204, $response->getStatusCode());
- $this->assertNull(
- $this->sessionRepo->findByToken('existing-session')
- );
- $this->assertStringContainsString(
- 'auth_token=;',
- $response->getHeaderLine('Set-Cookie')
- );
- }
-
- public function test_me_returns_current_user(): void
- {
- $user = new User(
- id: 5,
- email: new EmailAddress('me@test.com'),
- passwordHash: '',
- isAdmin: true,
- );
- $request = new ServerRequestFactory()
- ->createServerRequest('GET', 'http://localhost/api/auth/me')
- ->withAttribute('user', $user);
-
- $response = $this->controller->me($request, new Response());
-
- $this->assertEquals(200, $response->getStatusCode());
- $body = json_decode($response->getBody(), true);
- $this->assertEquals(5, $body['user']['id']);
- $this->assertEquals('me@test.com', $body['user']['email']);
- $this->assertTrue($body['user']['isAdmin']);
- }
-}
diff --git a/tests/e2e/Controllers/PlanControllerTest.php b/tests/e2e/Controllers/PlanControllerTest.php
index cf0eeb0..ee51012 100644
--- a/tests/e2e/Controllers/PlanControllerTest.php
+++ b/tests/e2e/Controllers/PlanControllerTest.php
@@ -8,7 +8,6 @@ use App\Plan\UseCases\CreatePlan;
use App\ScheduledNode\UseCases\CreateScheduledNode;
use App\Text\CreateTextDto;
use App\User\UseCases\CreateUserDto;
-use App\User\User;
use App\ValueObjects\EmailAddress;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ServerRequestInterface;
@@ -30,7 +29,6 @@ class PlanControllerTest extends TestCase
private FakeScheduledNodeRepository $scheduledNodeRepo;
private CreatePlan $createPlan;
private PlanController $controller;
- private User $user;
public function setUp(): void
{
@@ -40,9 +38,8 @@ class PlanControllerTest extends TestCase
$this->nodeRepo = new FakeNodeRepository();
$this->scheduledNodeRepo = new FakeScheduledNodeRepository();
- $this->user = $this->userRepo->create(new CreateUserDto(
+ $this->userRepo->create(new CreateUserDto(
email: new EmailAddress('test@test.com'),
- passwordHash: '',
));
$text = $this->textRepo->create(new CreateTextDto('testname'));
$this->nodeRepo->create(new CreateNodeDto(
@@ -71,7 +68,6 @@ class PlanControllerTest extends TestCase
return new ServerRequestFactory()
->createServerRequest('POST', 'http://localhost/api/plans')
->withHeader('Content-Type', 'application/json')
- ->withAttribute('user', $this->user)
->withBody($body);
}
@@ -79,6 +75,7 @@ class PlanControllerTest extends TestCase
{
$response = $this->controller->createPlan(
$this->makeRequest([
+ 'userId' => 0,
'textId' => 0,
'name' => 'My Plan',
'dateStart' => '2025-01-01',
@@ -94,33 +91,29 @@ class PlanControllerTest extends TestCase
$this->assertEquals('My Plan', $body['name']);
}
- public function test_create_plan_returns_401_when_no_user(): void
+ public function test_create_plan_returns_400_when_user_id_missing(): void
{
- $requestWithoutUser = new ServerRequestFactory()
- ->createServerRequest('POST', 'http://localhost/api/plans')
- ->withHeader('Content-Type', 'application/json')
- ->withBody(
- new StreamFactory()->createStream(json_encode([
- 'textId' => 0,
- 'name' => 'My Plan',
- 'dateStart' => '2025-01-01',
- 'dateEnd' => '2025-01-01',
- ]))
- );
-
$response = $this->controller->createPlan(
- $requestWithoutUser,
+ $this->makeRequest([
+ 'textId' => 0,
+ 'name' => 'My Plan',
+ 'dateStart' => '2025-01-01',
+ 'dateEnd' => '2025-01-01',
+ ]),
new Response(),
$this->createPlan,
);
- $this->assertEquals(401, $response->getStatusCode());
+ $this->assertEquals(400, $response->getStatusCode());
+ $body = json_decode($response->getBody(), true);
+ $this->assertArrayHasKey('error', $body);
}
public function test_create_plan_returns_400_when_text_id_missing(): void
{
$response = $this->controller->createPlan(
$this->makeRequest([
+ 'userId' => 0,
'name' => 'My Plan',
'dateStart' => '2025-01-01',
'dateEnd' => '2025-01-01',
@@ -138,6 +131,7 @@ class PlanControllerTest extends TestCase
{
$response = $this->controller->createPlan(
$this->makeRequest([
+ 'userId' => 0,
'textId' => 0,
'dateStart' => '2025-01-01',
'dateEnd' => '2025-01-01',
@@ -155,6 +149,7 @@ class PlanControllerTest extends TestCase
{
$response = $this->controller->createPlan(
$this->makeRequest([
+ 'userId' => 0,
'textId' => 0,
'name' => 'My Plan',
'dateEnd' => '2025-01-01',
@@ -172,6 +167,7 @@ class PlanControllerTest extends TestCase
{
$response = $this->controller->createPlan(
$this->makeRequest([
+ 'userId' => 0,
'textId' => 0,
'name' => 'My Plan',
'dateStart' => '2025-01-01',
@@ -189,6 +185,7 @@ class PlanControllerTest extends TestCase
{
$response = $this->controller->createPlan(
$this->makeRequest([
+ 'userId' => 0,
'textId' => 0,
'name' => 'My Plan',
'dateStart' => '2025-01-02',
@@ -203,10 +200,30 @@ class PlanControllerTest extends TestCase
$this->assertArrayHasKey('error', $body);
}
+ public function test_create_plan_returns_404_when_user_not_found(): void
+ {
+ $response = $this->controller->createPlan(
+ $this->makeRequest([
+ 'userId' => 99,
+ 'textId' => 0,
+ 'name' => 'My Plan',
+ 'dateStart' => '2025-01-01',
+ 'dateEnd' => '2025-01-01',
+ ]),
+ new Response(),
+ $this->createPlan,
+ );
+
+ $this->assertEquals(404, $response->getStatusCode());
+ $body = json_decode($response->getBody(), true);
+ $this->assertArrayHasKey('error', $body);
+ }
+
public function test_create_plan_returns_404_when_text_not_found(): void
{
$response = $this->controller->createPlan(
$this->makeRequest([
+ 'userId' => 0,
'textId' => 99,
'name' => 'My Plan',
'dateStart' => '2025-01-01',
@@ -225,6 +242,7 @@ class PlanControllerTest extends TestCase
{
$this->controller->createPlan(
$this->makeRequest([
+ 'userId' => 0,
'textId' => 0,
'name' => 'Persistent Plan',
'dateStart' => '2025-01-01',
@@ -243,6 +261,7 @@ class PlanControllerTest extends TestCase
{
$this->controller->createPlan(
$this->makeRequest([
+ 'userId' => 0,
'textId' => 0,
'name' => 'Scheduling Plan',
'dateStart' => '2025-01-01',
diff --git a/views/templates/admin.php b/views/templates/admin.php
index f1fc83d..7f463d4 100644
--- a/views/templates/admin.php
+++ b/views/templates/admin.php
@@ -4,8 +4,6 @@
Daily Goals - Admin
-
Texts
-