Compare commits
No commits in common. "38d72ba1a840496557621b93179e83ea708bf586" and "8eb0f2366b1dc64eede28b11a75752b8fef8df24" have entirely different histories.
38d72ba1a8
...
8eb0f2366b
36 changed files with 138 additions and 1186 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -2,5 +2,3 @@ vendor/
|
||||||
node_modules/
|
node_modules/
|
||||||
data/*.json
|
data/*.json
|
||||||
.direnv/
|
.direnv/
|
||||||
cypress/screenshots/
|
|
||||||
cypress/videos/
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://opencode.ai/config.json",
|
|
||||||
"plugin": [
|
|
||||||
"caveman",
|
|
||||||
"caveman-opencode-plugin@latest"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
# Project context
|
|
||||||
|
|
||||||
Read these on every session. Rules in them override defaults.
|
|
||||||
|
|
||||||
@ai/shared.md
|
|
||||||
@ai/backend-context.md
|
|
||||||
@ai/frontend-context.md
|
|
||||||
|
|
@ -16,14 +16,11 @@
|
||||||
<mxCell id="UlVOh7WOaItsqOB8hf6W-7" edge="1" parent="1" source="UlVOh7WOaItsqOB8hf6W-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" target="UlVOh7WOaItsqOB8hf6W-2">
|
<mxCell id="UlVOh7WOaItsqOB8hf6W-7" edge="1" parent="1" source="UlVOh7WOaItsqOB8hf6W-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" target="UlVOh7WOaItsqOB8hf6W-2">
|
||||||
<mxGeometry relative="1" as="geometry" />
|
<mxGeometry relative="1" as="geometry" />
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="UlVOh7WOaItsqOB8hf6W-21" edge="1" parent="1" source="UlVOh7WOaItsqOB8hf6W-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" target="UlVOh7WOaItsqOB8hf6W-9">
|
|
||||||
<mxGeometry relative="1" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="UlVOh7WOaItsqOB8hf6W-5" parent="1" style="whiteSpace=wrap;html=1;aspect=fixed;" value="Scheduled Node" vertex="1">
|
<mxCell id="UlVOh7WOaItsqOB8hf6W-5" parent="1" style="whiteSpace=wrap;html=1;aspect=fixed;" value="Scheduled Node" vertex="1">
|
||||||
<mxGeometry height="80" width="80" x="610" y="290" as="geometry" />
|
<mxGeometry height="80" width="80" x="610" y="290" as="geometry" />
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="UlVOh7WOaItsqOB8hf6W-8" parent="1" style="whiteSpace=wrap;html=1;aspect=fixed;" value="Text" vertex="1">
|
<mxCell id="UlVOh7WOaItsqOB8hf6W-8" parent="1" style="whiteSpace=wrap;html=1;aspect=fixed;" value="Text" vertex="1">
|
||||||
<mxGeometry height="80" width="80" x="450" y="90" as="geometry" />
|
<mxGeometry height="80" width="80" x="240" y="60" as="geometry" />
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="UlVOh7WOaItsqOB8hf6W-12" edge="1" parent="1" source="UlVOh7WOaItsqOB8hf6W-9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="UlVOh7WOaItsqOB8hf6W-8">
|
<mxCell id="UlVOh7WOaItsqOB8hf6W-12" edge="1" parent="1" source="UlVOh7WOaItsqOB8hf6W-9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="UlVOh7WOaItsqOB8hf6W-8">
|
||||||
<mxGeometry relative="1" as="geometry" />
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
|
@ -31,14 +28,14 @@
|
||||||
<mxCell id="UlVOh7WOaItsqOB8hf6W-14" edge="1" parent="1" source="UlVOh7WOaItsqOB8hf6W-9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;" target="UlVOh7WOaItsqOB8hf6W-9" value="">
|
<mxCell id="UlVOh7WOaItsqOB8hf6W-14" edge="1" parent="1" source="UlVOh7WOaItsqOB8hf6W-9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;" target="UlVOh7WOaItsqOB8hf6W-9" value="">
|
||||||
<mxGeometry relative="1" as="geometry">
|
<mxGeometry relative="1" as="geometry">
|
||||||
<Array as="points">
|
<Array as="points">
|
||||||
<mxPoint x="800" y="110" />
|
<mxPoint x="590" y="80" />
|
||||||
<mxPoint x="800" y="150" />
|
<mxPoint x="590" y="120" />
|
||||||
</Array>
|
</Array>
|
||||||
<mxPoint x="800" y="150" as="targetPoint" />
|
<mxPoint x="590" y="120" as="targetPoint" />
|
||||||
</mxGeometry>
|
</mxGeometry>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="UlVOh7WOaItsqOB8hf6W-9" parent="1" style="whiteSpace=wrap;html=1;aspect=fixed;" value="Node" vertex="1">
|
<mxCell id="UlVOh7WOaItsqOB8hf6W-9" parent="1" style="whiteSpace=wrap;html=1;aspect=fixed;" value="Node" vertex="1">
|
||||||
<mxGeometry height="80" width="80" x="610" y="90" as="geometry" />
|
<mxGeometry height="80" width="80" x="400" y="60" as="geometry" />
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="UlVOh7WOaItsqOB8hf6W-19" edge="1" parent="1" source="UlVOh7WOaItsqOB8hf6W-17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="UlVOh7WOaItsqOB8hf6W-1">
|
<mxCell id="UlVOh7WOaItsqOB8hf6W-19" edge="1" parent="1" source="UlVOh7WOaItsqOB8hf6W-17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="UlVOh7WOaItsqOB8hf6W-1">
|
||||||
<mxGeometry relative="1" as="geometry" />
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
# Backend context
|
|
||||||
|
|
||||||
> Read `ai/shared.md` first. This file only covers backend-specific rules.
|
|
||||||
|
|
||||||
## Project Context
|
|
||||||
|
|
||||||
**Stack:** PHP 8.5, Slim 4, PHP-DI/Slim-Bridge, PHPUnit 13, Composer.
|
|
||||||
Persistence is JSON-file based (see `Json*Repository` classes); no ORM.
|
|
||||||
**Architecture:** Domain-Driven Design. Code is organized by domain entity
|
|
||||||
under `app/` (Auth, Node, Plan, ScheduledNode, Text, User, View,
|
|
||||||
ValueObjects) into Entities, DTOs, Repositories, Use Cases, and Fakes
|
|
||||||
(in-memory repos for tests).
|
|
||||||
|
|
||||||
## Code patterns
|
|
||||||
|
|
||||||
- Look at similar entities (e.g. `Node`, `Text`) for reference
|
|
||||||
- Entities: constructor with properties, getters
|
|
||||||
- DTOs: simple data containers for creation (e.g. `CreateTextDto`)
|
|
||||||
- Repositories: interfaces that define data access
|
|
||||||
- Do not write unit tests for concrete repository implementations
|
|
||||||
(e.g. `JsonNodeRepository`). 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
|
|
||||||
|
|
||||||
## PHP rules
|
|
||||||
|
|
||||||
- Imports: always put `use` statements at the top of the file, never use inline
|
|
||||||
imports (e.g. `\App\Foo\Bar::class`)
|
|
||||||
- Closures: never use arrow functions (`fn () =>`) - always use regular
|
|
||||||
anonymous functions (`function () { return ...; }`)
|
|
||||||
|
|
||||||
## Pre-commit
|
|
||||||
|
|
||||||
Run `php-cs-fixer fix` on worked-on directories before committing (uses the
|
|
||||||
existing `.php-cs-fixer.dist.php` config).
|
|
||||||
68
ai/backend_prompt_template.md
Normal file
68
ai/backend_prompt_template.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Entity Creation Prompt Template
|
||||||
|
|
||||||
|
Follow the existing patterns in this codebase to:
|
||||||
|
- create a new entity called [EntityName].
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- The entity encapsulates [one or more Entities]
|
||||||
|
- Include [any other fields]
|
||||||
|
|
||||||
|
Process (TDD - Test Driven Development):
|
||||||
|
1. Write a test first
|
||||||
|
2. Run the test to confirm it fails
|
||||||
|
3. Implement the code to make the test pass
|
||||||
|
4. Run the test to confirm it passes
|
||||||
|
5. Repeat for each new behavior
|
||||||
|
|
||||||
|
Code patterns to follow:
|
||||||
|
- First, explore the codebase to understand existing entity patterns
|
||||||
|
- Look at similar entities (e.g. Node, Text, etc.) for reference
|
||||||
|
- 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
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
- Commits are for reviewing and documenting the development of code
|
||||||
|
- Don't wait to commit - commit as you go
|
||||||
|
- Run `php-cs-fixer fix` on worked on directories before committing
|
||||||
|
|
||||||
|
Branch naming:
|
||||||
|
- Use kebab-case (e.g., presenting-track, agenda-slots)
|
||||||
|
- Use descriptive feature names
|
||||||
|
- Examples: "presenting-track", "agenda-slots", "confirm-application"
|
||||||
|
- Or use type/description: "feature/presenting-track", "fix/bug-name"
|
||||||
|
- NEVER work directly on master/main - always create and work on a branch
|
||||||
|
|
||||||
|
Do not push anything. Make commits as you go.
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
# Frontend context
|
|
||||||
|
|
||||||
> Read `ai/shared.md` first. This file only covers frontend-specific rules.
|
|
||||||
|
|
||||||
## Project Context
|
|
||||||
|
|
||||||
**Stack:** vanilla PHP templates in `views/templates/`, plain ES JavaScript in
|
|
||||||
`public/js/`, no framework, no build step. Cypress 15 for E2E.
|
|
||||||
**Entry point:** `public/index.php` (Slim app); page templates are rendered
|
|
||||||
via the existing templating layer.
|
|
||||||
|
|
||||||
## Code patterns
|
|
||||||
|
|
||||||
- Look at existing pages (`home.php`/`home.js`, `text.php`/`text.js`,
|
|
||||||
`today.php`/`today.js`) for reference before writing anything
|
|
||||||
- **Templates:** `views/templates/<page>.php`, one file per page
|
|
||||||
- **Page JS:** `public/js/<page>.js`, one file per page, paired with the
|
|
||||||
matching template
|
|
||||||
- **Testing:** Cypress E2E only, mirror existing `cypress/e2e/*.cy.js` style
|
|
||||||
(note: this project uses `.cy.js`, not `.cy.ts`)
|
|
||||||
- **Imports / script tags:** keep at the top of the file
|
|
||||||
- **Variable names:** explicit, descriptive (e.g. `text` not `t`)
|
|
||||||
|
|
||||||
## Pre-commit
|
|
||||||
|
|
||||||
No JS formatter or linter is configured yet; format manually for consistency
|
|
||||||
with surrounding files. (TODO: wire up format/lint when added.)
|
|
||||||
|
|
||||||
## Note on commit granularity
|
|
||||||
|
|
||||||
Frontend changes are often a template plus its page-level JS counterpart -
|
|
||||||
commit them together as a single logical unit, per the "one logical change
|
|
||||||
per commit" rule in `shared.md`.
|
|
||||||
52
ai/frontend_prompt_template.md
Normal file
52
ai/frontend_prompt_template.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Frontend Prompt Template
|
||||||
|
|
||||||
|
Follow the existing patterns in this codebase to:
|
||||||
|
- xxxxxxxx
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- xxxxx
|
||||||
|
|
||||||
|
Process (TDD - Test Driven Development):
|
||||||
|
1. Write a test first
|
||||||
|
2. Run the test to confirm it fails
|
||||||
|
3. Implement the code to make the test pass
|
||||||
|
4. Run the test to confirm it passes
|
||||||
|
5. Repeat for each new behavior
|
||||||
|
|
||||||
|
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
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
- Commits are for reviewing and documenting the development of code
|
||||||
|
- Don't wait to commit - commit as you go
|
||||||
|
|
||||||
|
Branch naming:
|
||||||
|
- Use kebab-case (e.g., node-page text-page)
|
||||||
|
- Use descriptive feature names
|
||||||
|
- NEVER work directly on master - always create and work on a branch
|
||||||
|
|
||||||
|
Do not push anything. Make commits as you go.
|
||||||
74
ai/shared.md
74
ai/shared.md
|
|
@ -1,74 +0,0 @@
|
||||||
# Shared rules
|
|
||||||
|
|
||||||
Rules that apply to both backend and frontend work in this repo. Stack-specific
|
|
||||||
guides (`backend-context.md`, `frontend-context.md`) extend these.
|
|
||||||
|
|
||||||
## Process (TDD)
|
|
||||||
|
|
||||||
0. Before editing any file, ensure you are on a feature branch
|
|
||||||
(`git status` to confirm). If on master/main, create a branch
|
|
||||||
first.
|
|
||||||
1. Write the test first
|
|
||||||
2. Run the test to confirm it fails
|
|
||||||
3. Commit the failing test (the "tests committed first" rule in
|
|
||||||
action - the test commit precedes the implementation commit, not
|
|
||||||
merely the implementation lines)
|
|
||||||
4. Implement the code to make the test pass
|
|
||||||
5. Run the test to confirm it passes
|
|
||||||
6. Commit the implementation
|
|
||||||
7. Repeat for each new behavior
|
|
||||||
|
|
||||||
## Code style
|
|
||||||
|
|
||||||
- Lines should not exceed 80 columns, but should use up to 80 columns when
|
|
||||||
possible - do not split lines unnecessarily
|
|
||||||
- Variable names: use explicit, descriptive names - never single-letter or
|
|
||||||
abbreviated variables (e.g. `$text` not `$t`, `$node` not `$n`)
|
|
||||||
- Method/function/constructor parameters: do not use default values - every
|
|
||||||
call site must pass every argument explicitly. This eliminates a class of
|
|
||||||
bugs where an unintended default silently slips through (e.g. an
|
|
||||||
`isAdmin=false` or an empty `passwordHash`). Apply the same rule in tests
|
|
||||||
and fakes - if a helper accepts a value, every caller must supply it.
|
|
||||||
- First, explore the codebase to understand existing patterns - look at similar
|
|
||||||
files for reference before writing anything
|
|
||||||
- Never use em dashes (—) in code, comments, or docblocks - use hyphens (-)
|
|
||||||
instead
|
|
||||||
|
|
||||||
## Git commit style
|
|
||||||
|
|
||||||
- Present tense, imperative mood (add, create, wire, fix, test)
|
|
||||||
- Lowercase
|
|
||||||
- Short (3-6 words)
|
|
||||||
- Match patterns found in git history
|
|
||||||
- Do not add any section mentioning claude as a coauthor
|
|
||||||
- Add a commit body when the subject alone cannot convey the change - e.g.
|
|
||||||
non-obvious motivation, multi-file coordination, or notable complexity
|
|
||||||
- Body: wrap at ~72 columns, separated from subject by a blank line, explain
|
|
||||||
the why and any non-obvious what
|
|
||||||
- Skip the body for trivial or self-explanatory commits
|
|
||||||
|
|
||||||
## Git commits
|
|
||||||
|
|
||||||
- Tests should be committed first, before implementation
|
|
||||||
- One logical change per commit - a commit may span multiple files when they
|
|
||||||
form a single logical unit (e.g. a use case with its request and exception,
|
|
||||||
or a template with its page-level JS)
|
|
||||||
- Keep commits focused: not one file per commit, not unrelated work batched
|
|
||||||
- Make commits frequent - commit each meaningful logical step as you go
|
|
||||||
- Commits are for reviewing and documenting the development of code
|
|
||||||
- When the formatter or linter modifies files outside your intended
|
|
||||||
change, either `git restore` them or land them as a separate
|
|
||||||
`format <area>` / `lint <area>` commit - never bundle drive-by
|
|
||||||
formatter churn into a feature commit
|
|
||||||
- If pre-commit lint fails on code you did not touch, do not bundle
|
|
||||||
the fix - either land the unrelated fix as its own commit first, or
|
|
||||||
note the pre-existing failure and proceed
|
|
||||||
|
|
||||||
## Branching
|
|
||||||
|
|
||||||
- Use kebab-case (e.g. `text-page`, `scheduled-node`, `auth-flow`)
|
|
||||||
- Use descriptive feature names
|
|
||||||
- Or use type/description: `feature/text-page`, `fix/bug-name`
|
|
||||||
- NEVER work directly on master/main - always create and work on a branch
|
|
||||||
|
|
||||||
Do not push anything. Make commits as you go.
|
|
||||||
|
|
@ -90,29 +90,4 @@ class JsonPlanRepository implements PlanRepository
|
||||||
|
|
||||||
return $maxId + 1;
|
return $maxId + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findByUser(User $user): array
|
|
||||||
{
|
|
||||||
$plans = array_filter(
|
|
||||||
$this->readPlans(),
|
|
||||||
function ($data) use ($user) {
|
|
||||||
return $data['userId'] === $user->getId();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return array_map(
|
|
||||||
function ($data) {
|
|
||||||
$user = $this->userRepository->find($data['userId']);
|
|
||||||
if ($user === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Plan(
|
|
||||||
id: $data['id'],
|
|
||||||
name: $data['name'],
|
|
||||||
user: $user,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
$plans
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,8 @@
|
||||||
|
|
||||||
namespace App\Plan;
|
namespace App\Plan;
|
||||||
|
|
||||||
use App\User\User;
|
|
||||||
|
|
||||||
interface PlanRepository
|
interface PlanRepository
|
||||||
{
|
{
|
||||||
public function create(CreatePlanDto $dto): Plan;
|
public function create(CreatePlanDto $dto): Plan;
|
||||||
public function find(int $id): ?Plan;
|
public function find(int $id): ?Plan;
|
||||||
/**
|
|
||||||
* @return Plan[]
|
|
||||||
*/
|
|
||||||
public function findByUser(User $user): array;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,6 @@ class CreatePlan
|
||||||
new CreateScheduledNodeRequest(
|
new CreateScheduledNodeRequest(
|
||||||
date: $scheduledDate->format('Y-m-d'),
|
date: $scheduledDate->format('Y-m-d'),
|
||||||
planId: $plan->getId(),
|
planId: $plan->getId(),
|
||||||
nodeId: $node->getId(),
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
namespace App\ScheduledNode;
|
namespace App\ScheduledNode;
|
||||||
|
|
||||||
use App\Node\Node;
|
|
||||||
use App\Plan\Plan;
|
use App\Plan\Plan;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
|
|
||||||
|
|
@ -11,6 +10,5 @@ class CreateScheduledNodeDto
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public DateTimeImmutable $date,
|
public DateTimeImmutable $date,
|
||||||
public Plan $plan,
|
public Plan $plan,
|
||||||
public Node $node,
|
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,12 @@
|
||||||
|
|
||||||
namespace App\ScheduledNode;
|
namespace App\ScheduledNode;
|
||||||
|
|
||||||
use App\Node\NodeRepository;
|
|
||||||
use App\Plan\PlanRepository;
|
|
||||||
use App\User\User;
|
|
||||||
use DateTimeImmutable;
|
|
||||||
|
|
||||||
class JsonScheduledNodeRepository implements ScheduledNodeRepository
|
class JsonScheduledNodeRepository implements ScheduledNodeRepository
|
||||||
{
|
{
|
||||||
private string $filePath;
|
private string $filePath;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct()
|
||||||
private PlanRepository $planRepo,
|
{
|
||||||
private NodeRepository $nodeRepo,
|
|
||||||
) {
|
|
||||||
$this->filePath = __DIR__ . '/../../data/scheduledNodes.json';
|
$this->filePath = __DIR__ . '/../../data/scheduledNodes.json';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,8 +20,6 @@ class JsonScheduledNodeRepository implements ScheduledNodeRepository
|
||||||
'id' => $id,
|
'id' => $id,
|
||||||
'date' => $dto->date->format('Y-m-d'),
|
'date' => $dto->date->format('Y-m-d'),
|
||||||
'planId' => $dto->plan->getId(),
|
'planId' => $dto->plan->getId(),
|
||||||
'nodeId' => $dto->node->getId(),
|
|
||||||
'completed' => false,
|
|
||||||
];
|
];
|
||||||
$this->writeScheduledNodes($scheduledNodes);
|
$this->writeScheduledNodes($scheduledNodes);
|
||||||
|
|
||||||
|
|
@ -36,8 +27,6 @@ class JsonScheduledNodeRepository implements ScheduledNodeRepository
|
||||||
id: $id,
|
id: $id,
|
||||||
date: $dto->date,
|
date: $dto->date,
|
||||||
plan: $dto->plan,
|
plan: $dto->plan,
|
||||||
node: $dto->node,
|
|
||||||
completed: false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,34 +64,4 @@ class JsonScheduledNodeRepository implements ScheduledNodeRepository
|
||||||
|
|
||||||
return $maxId + 1;
|
return $maxId + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findByUser(User $user): array
|
|
||||||
{
|
|
||||||
$allScheduledNodes = $this->readScheduledNodes();
|
|
||||||
$planIds = array_map(
|
|
||||||
function ($plan) {
|
|
||||||
return $plan->getId();
|
|
||||||
},
|
|
||||||
$this->planRepo->findByUser($user)
|
|
||||||
);
|
|
||||||
$usersScheduledNodes = array_filter(
|
|
||||||
$allScheduledNodes,
|
|
||||||
function ($node) use ($planIds) {
|
|
||||||
return in_array($node['planId'], $planIds);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return array_map(
|
|
||||||
function ($data) {
|
|
||||||
return new ScheduledNode(
|
|
||||||
id: $data['id'],
|
|
||||||
date: new DateTimeImmutable($data['date']),
|
|
||||||
plan: $this->planRepo->find($data['planId']),
|
|
||||||
node: $this->nodeRepo->find($data['nodeId']),
|
|
||||||
completed: $data['completed']
|
|
||||||
);
|
|
||||||
},
|
|
||||||
$usersScheduledNodes
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
namespace App\ScheduledNode;
|
namespace App\ScheduledNode;
|
||||||
|
|
||||||
use App\Node\Node;
|
|
||||||
use App\Plan\Plan;
|
use App\Plan\Plan;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
|
|
||||||
|
|
@ -12,8 +11,6 @@ class ScheduledNode
|
||||||
private int $id,
|
private int $id,
|
||||||
private DateTimeImmutable $date,
|
private DateTimeImmutable $date,
|
||||||
private Plan $plan,
|
private Plan $plan,
|
||||||
private Node $node,
|
|
||||||
private bool $completed,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function getId(): int
|
public function getId(): int
|
||||||
|
|
@ -30,19 +27,4 @@ class ScheduledNode
|
||||||
{
|
{
|
||||||
return $this->date;
|
return $this->date;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getNode(): Node
|
|
||||||
{
|
|
||||||
return $this->node;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCompleted(): bool
|
|
||||||
{
|
|
||||||
return $this->completed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setCompleted(bool $complete): void
|
|
||||||
{
|
|
||||||
$this->completed = $complete;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\ScheduledNode;
|
|
||||||
|
|
||||||
use App\Exceptions\BadRequestException;
|
|
||||||
use App\ScheduledNode\UseCases\GetTodaysSchedule;
|
|
||||||
use App\ScheduledNode\UseCases\GetTodaysScheduleRequest;
|
|
||||||
use App\User\User;
|
|
||||||
use DomainException;
|
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
|
||||||
|
|
||||||
class ScheduledNodeController
|
|
||||||
{
|
|
||||||
public function getScheduledNodes(
|
|
||||||
Request $request,
|
|
||||||
Response $response,
|
|
||||||
GetTodaysSchedule $getTodaysSchedule,
|
|
||||||
): Response {
|
|
||||||
$user = $request->getAttribute('user');
|
|
||||||
if (!$user instanceof User) {
|
|
||||||
$response->getBody()->write(
|
|
||||||
json_encode(['error' => 'unauthenticated'])
|
|
||||||
);
|
|
||||||
return $response->withStatus(401)
|
|
||||||
->withHeader('Content-Type', 'application/json');
|
|
||||||
}
|
|
||||||
|
|
||||||
$queryParams = $request->getQueryParams();
|
|
||||||
$date = $queryParams['date'] ?? null;
|
|
||||||
if ($date === '') {
|
|
||||||
$date = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$scheduledNodes = $getTodaysSchedule->execute(
|
|
||||||
new GetTodaysScheduleRequest(
|
|
||||||
date: $date,
|
|
||||||
userId: $user->getId(),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} catch (BadRequestException $exception) {
|
|
||||||
$response->getBody()->write(
|
|
||||||
json_encode(['error' => $exception->getMessage()])
|
|
||||||
);
|
|
||||||
return $response->withStatus(400)
|
|
||||||
->withHeader('Content-Type', 'application/json');
|
|
||||||
} catch (DomainException $exception) {
|
|
||||||
$response->getBody()->write(
|
|
||||||
json_encode(['error' => $exception->getMessage()])
|
|
||||||
);
|
|
||||||
return $response->withStatus(404)
|
|
||||||
->withHeader('Content-Type', 'application/json');
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = array_values(array_map(
|
|
||||||
function (ScheduledNode $scheduledNode) {
|
|
||||||
return [
|
|
||||||
'id' => $scheduledNode->getId(),
|
|
||||||
'date' => $scheduledNode->getDate()->format('Y-m-d'),
|
|
||||||
'planName' => $scheduledNode->getPlan()->getName(),
|
|
||||||
'nodeTitle' => $scheduledNode->getNode()->getTitle(),
|
|
||||||
'completed' => $scheduledNode->getCompleted(),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
$scheduledNodes,
|
|
||||||
));
|
|
||||||
|
|
||||||
$response->getBody()->write(json_encode($data));
|
|
||||||
return $response->withStatus(200)
|
|
||||||
->withHeader('Content-Type', 'application/json');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,13 +2,7 @@
|
||||||
|
|
||||||
namespace App\ScheduledNode;
|
namespace App\ScheduledNode;
|
||||||
|
|
||||||
use App\User\User;
|
|
||||||
|
|
||||||
interface ScheduledNodeRepository
|
interface ScheduledNodeRepository
|
||||||
{
|
{
|
||||||
public function create(CreateScheduledNodeDto $dto): ScheduledNode;
|
public function create(CreateScheduledNodeDto $dto): ScheduledNode;
|
||||||
/**
|
|
||||||
* @return ScheduledNode[]
|
|
||||||
*/
|
|
||||||
public function findByUser(User $user): array;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
namespace App\ScheduledNode\UseCases;
|
namespace App\ScheduledNode\UseCases;
|
||||||
|
|
||||||
use App\Exceptions\BadRequestException;
|
use App\Exceptions\BadRequestException;
|
||||||
use App\Node\NodeRepository;
|
|
||||||
use App\Plan\PlanRepository;
|
use App\Plan\PlanRepository;
|
||||||
use App\ScheduledNode\ScheduledNode;
|
use App\ScheduledNode\ScheduledNode;
|
||||||
use App\ScheduledNode\CreateScheduledNodeDto;
|
use App\ScheduledNode\CreateScheduledNodeDto;
|
||||||
|
|
@ -16,7 +15,6 @@ class CreateScheduledNode
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ScheduledNodeRepository $scheduledNodeRepo,
|
private ScheduledNodeRepository $scheduledNodeRepo,
|
||||||
private PlanRepository $planRepo,
|
private PlanRepository $planRepo,
|
||||||
private NodeRepository $nodeRepo,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -26,40 +24,24 @@ class CreateScheduledNode
|
||||||
public function execute(
|
public function execute(
|
||||||
CreateScheduledNodeRequest $request
|
CreateScheduledNodeRequest $request
|
||||||
): ScheduledNode {
|
): ScheduledNode {
|
||||||
$nodeId = $request->nodeId;
|
if ($request->date === null) {
|
||||||
$planId = $request->planId;
|
|
||||||
$date = $request->date;
|
|
||||||
if ($date === null) {
|
|
||||||
throw new BadRequestException('date is required');
|
throw new BadRequestException('date is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($planId === null) {
|
if ($request->planId === null) {
|
||||||
throw new BadRequestException('planId is required');
|
throw new BadRequestException('planId is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($nodeId === null) {
|
$id = $request->planId;
|
||||||
throw new BadRequestException('nodeId is required');
|
$plan = $this->planRepo->find($id);
|
||||||
}
|
|
||||||
|
|
||||||
$plan = $this->planRepo->find($planId);
|
|
||||||
if ($plan === null) {
|
if ($plan === null) {
|
||||||
throw new DomainException(
|
throw new DomainException("Plan with id: $id doesnt exist");
|
||||||
"Plan with id: $planId doesnt exist"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$node = $this->nodeRepo->find($nodeId);
|
|
||||||
if ($node === null) {
|
|
||||||
throw new DomainException(
|
|
||||||
"Node with id: $nodeId doesnt exist"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->scheduledNodeRepo->create(
|
return $this->scheduledNodeRepo->create(
|
||||||
new CreateScheduledNodeDto(
|
new CreateScheduledNodeDto(
|
||||||
date: new DateTimeImmutable($date),
|
date: new DateTimeImmutable($request->date),
|
||||||
plan: $plan,
|
plan: $plan,
|
||||||
node: $node,
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,5 @@ class CreateScheduledNodeRequest
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public ?string $date,
|
public ?string $date,
|
||||||
public ?int $planId,
|
public ?int $planId,
|
||||||
public ?int $nodeId,
|
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\ScheduledNode\UseCases;
|
|
||||||
|
|
||||||
use App\Exceptions\BadRequestException;
|
|
||||||
use App\ScheduledNode\ScheduledNode;
|
|
||||||
use App\ScheduledNode\ScheduledNodeRepository;
|
|
||||||
use App\User\UserRepository;
|
|
||||||
use DateTimeImmutable;
|
|
||||||
use DomainException;
|
|
||||||
|
|
||||||
class GetTodaysSchedule
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private UserRepository $userRepo,
|
|
||||||
private ScheduledNodeRepository $scheduledNodeRepo,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return ScheduledNode[]
|
|
||||||
*
|
|
||||||
* @throws BadRequestException
|
|
||||||
* @throws DomainException
|
|
||||||
*/
|
|
||||||
public function execute(GetTodaysScheduleRequest $request): array
|
|
||||||
{
|
|
||||||
if ($request->date === null) {
|
|
||||||
throw new BadRequestException('date is required');
|
|
||||||
}
|
|
||||||
if ($request->userId === null) {
|
|
||||||
throw new BadRequestException('userId is required');
|
|
||||||
}
|
|
||||||
$date = new DateTimeImmutable($request->date);
|
|
||||||
$userId = $request->userId;
|
|
||||||
$user = $this->userRepo->find($userId);
|
|
||||||
if ($user === null) {
|
|
||||||
throw new DomainException(
|
|
||||||
"User with id: $userId doesnt exist"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$scheduledNodes = $this->scheduledNodeRepo->findByUser($user);
|
|
||||||
|
|
||||||
return array_filter(
|
|
||||||
$scheduledNodes,
|
|
||||||
function (ScheduledNode $node) use ($date) {
|
|
||||||
return $node->getDate() <= $date
|
|
||||||
&& $node->getCompleted() === false;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\ScheduledNode\UseCases;
|
|
||||||
|
|
||||||
class GetTodaysScheduleRequest
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public ?string $date,
|
|
||||||
public ?int $userId,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
@ -38,16 +38,6 @@ class ViewController
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function today(Response $response): Response
|
|
||||||
{
|
|
||||||
$html = file_get_contents(
|
|
||||||
__DIR__ . '/../../views/templates/today.php'
|
|
||||||
);
|
|
||||||
$response->getBody()->write($html);
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function login(Response $response): Response
|
public function login(Response $response): Response
|
||||||
{
|
{
|
||||||
$html = file_get_contents(
|
$html = file_get_contents(
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ use App\View\ViewController;
|
||||||
use App\Text\TextController;
|
use App\Text\TextController;
|
||||||
use App\Node\NodeController;
|
use App\Node\NodeController;
|
||||||
use App\Plan\PlanController;
|
use App\Plan\PlanController;
|
||||||
use App\ScheduledNode\ScheduledNodeController;
|
|
||||||
|
|
||||||
$container = require __DIR__ . '/container.php';
|
$container = require __DIR__ . '/container.php';
|
||||||
$app = Bridge::create($container);
|
$app = Bridge::create($container);
|
||||||
|
|
@ -28,7 +27,6 @@ $app->post('/api/auth/register', [AuthController::class, 'register']);
|
||||||
// Authenticated routes (any logged-in user)
|
// Authenticated routes (any logged-in user)
|
||||||
$app->group('', function (RouteCollectorProxy $group) {
|
$app->group('', function (RouteCollectorProxy $group) {
|
||||||
$group->get('/home', [ViewController::class, 'home']);
|
$group->get('/home', [ViewController::class, 'home']);
|
||||||
$group->get('/today', [ViewController::class, 'today']);
|
|
||||||
|
|
||||||
$group->post('/api/auth/logout', [AuthController::class, 'logout']);
|
$group->post('/api/auth/logout', [AuthController::class, 'logout']);
|
||||||
$group->get('/api/auth/me', [AuthController::class, 'me']);
|
$group->get('/api/auth/me', [AuthController::class, 'me']);
|
||||||
|
|
@ -45,11 +43,6 @@ $app->group('', function (RouteCollectorProxy $group) {
|
||||||
);
|
);
|
||||||
|
|
||||||
$group->post('/api/plans', [PlanController::class, 'createPlan']);
|
$group->post('/api/plans', [PlanController::class, 'createPlan']);
|
||||||
|
|
||||||
$group->get(
|
|
||||||
'/api/scheduled-nodes',
|
|
||||||
[ScheduledNodeController::class, 'getScheduledNodes']
|
|
||||||
);
|
|
||||||
})->add(AuthMiddleware::class);
|
})->add(AuthMiddleware::class);
|
||||||
|
|
||||||
// Admin-only routes
|
// Admin-only routes
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,10 @@ $container = new Container([
|
||||||
NodeRepository::class => DI\autowire(JsonNodeRepository::class),
|
NodeRepository::class => DI\autowire(JsonNodeRepository::class),
|
||||||
PlanRepository::class => DI\autowire(JsonPlanRepository::class),
|
PlanRepository::class => DI\autowire(JsonPlanRepository::class),
|
||||||
UserRepository::class => DI\autowire(JsonUserRepository::class),
|
UserRepository::class => DI\autowire(JsonUserRepository::class),
|
||||||
ScheduledNodeRepository::class
|
ScheduledNodeRepository::class =>
|
||||||
=> DI\autowire(JsonScheduledNodeRepository::class),
|
DI\autowire(JsonScheduledNodeRepository::class),
|
||||||
SessionRepository::class
|
SessionRepository::class =>
|
||||||
=> DI\autowire(JsonSessionRepository::class),
|
DI\autowire(JsonSessionRepository::class),
|
||||||
TokenGenerator::class => DI\autowire(RandomTokenGenerator::class),
|
TokenGenerator::class => DI\autowire(RandomTokenGenerator::class),
|
||||||
Clock::class => DI\autowire(SystemClock::class),
|
Clock::class => DI\autowire(SystemClock::class),
|
||||||
PasswordHasher::class => DI\autowire(BcryptPasswordHasher::class),
|
PasswordHasher::class => DI\autowire(BcryptPasswordHasher::class),
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"defaultMode": "full",
|
|
||||||
"features": {
|
|
||||||
"caveman": true,
|
|
||||||
"commit": ,
|
|
||||||
"review": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
describe('The today page', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.exec('npm run db:seed')
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
cy.exec('npm run db:wipe')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('redirects to login when not authenticated', () => {
|
|
||||||
cy.visit('/today')
|
|
||||||
cy.url().should('include', '/login')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('displays a Today heading when authenticated', () => {
|
|
||||||
cy.loginAsUser()
|
|
||||||
cy.visit('/today')
|
|
||||||
cy.get('h1').should('contain', 'Today')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has a list element for scheduled nodes', () => {
|
|
||||||
cy.loginAsUser()
|
|
||||||
cy.visit('/today')
|
|
||||||
cy.get('#scheduled-nodes-list').should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('home page links to the today page', () => {
|
|
||||||
cy.loginAsUser()
|
|
||||||
cy.visit('/home')
|
|
||||||
cy.get('a[href="/today"]').should('be.visible')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('lists scheduled nodes for today', () => {
|
|
||||||
const today = new Date()
|
|
||||||
const year = today.getFullYear()
|
|
||||||
const month = String(today.getMonth() + 1).padStart(2, '0')
|
|
||||||
const day = String(today.getDate()).padStart(2, '0')
|
|
||||||
const todayString = year + '-' + month + '-' + day
|
|
||||||
|
|
||||||
cy.loginAsUser()
|
|
||||||
cy.request({
|
|
||||||
method: 'POST',
|
|
||||||
url: '/api/plans',
|
|
||||||
body: {
|
|
||||||
textId: 0,
|
|
||||||
name: 'My reading plan',
|
|
||||||
dateStart: todayString,
|
|
||||||
dateEnd: todayString,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.intercept('GET', '/api/scheduled-nodes*')
|
|
||||||
.as('getScheduledNodes')
|
|
||||||
cy.visit('/today')
|
|
||||||
cy.wait('@getScheduledNodes').then((interception) => {
|
|
||||||
expect(interception.request.url).to.include(
|
|
||||||
'date=' + todayString
|
|
||||||
)
|
|
||||||
})
|
|
||||||
cy.get('#scheduled-nodes-list').should(
|
|
||||||
'contain',
|
|
||||||
'My reading plan'
|
|
||||||
)
|
|
||||||
cy.get('#scheduled-nodes-list').should('contain', 'Bereishis')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows an empty list when no nodes are scheduled today', () => {
|
|
||||||
cy.loginAsUser()
|
|
||||||
cy.intercept('GET', '/api/scheduled-nodes*')
|
|
||||||
.as('getScheduledNodes')
|
|
||||||
cy.visit('/today')
|
|
||||||
cy.wait('@getScheduledNodes')
|
|
||||||
cy.get('#scheduled-nodes-list li').should('have.length', 0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const scheduledNodesList = document.getElementById(
|
|
||||||
'scheduled-nodes-list'
|
|
||||||
);
|
|
||||||
|
|
||||||
function todayDateString() {
|
|
||||||
const today = new Date();
|
|
||||||
const year = today.getFullYear();
|
|
||||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(today.getDate()).padStart(2, '0');
|
|
||||||
return year + '-' + month + '-' + day;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadScheduledNodes() {
|
|
||||||
const date = todayDateString();
|
|
||||||
const response = await fetch(
|
|
||||||
'/api/scheduled-nodes?date=' + date,
|
|
||||||
{ credentials: 'same-origin' }
|
|
||||||
);
|
|
||||||
if (!response.ok) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const scheduledNodes = await response.json();
|
|
||||||
scheduledNodesList.innerHTML = scheduledNodes
|
|
||||||
.map((scheduledNode) =>
|
|
||||||
'<li>' + scheduledNode.planName + ': ' +
|
|
||||||
scheduledNode.nodeTitle + '</li>'
|
|
||||||
)
|
|
||||||
.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
loadScheduledNodes();
|
|
||||||
});
|
|
||||||
|
|
@ -5,13 +5,9 @@ namespace Tests\Fakes;
|
||||||
use App\Plan\CreatePlanDto;
|
use App\Plan\CreatePlanDto;
|
||||||
use App\Plan\Plan;
|
use App\Plan\Plan;
|
||||||
use App\Plan\PlanRepository;
|
use App\Plan\PlanRepository;
|
||||||
use App\User\User;
|
|
||||||
|
|
||||||
class FakePlanRepository implements PlanRepository
|
class FakePlanRepository implements PlanRepository
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var Plan[]
|
|
||||||
*/
|
|
||||||
private array $existingPlans = [];
|
private array $existingPlans = [];
|
||||||
|
|
||||||
public function create(CreatePlanDto $dto): Plan
|
public function create(CreatePlanDto $dto): Plan
|
||||||
|
|
@ -41,24 +37,4 @@ class FakePlanRepository implements PlanRepository
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findByUser(User $user): array
|
|
||||||
{
|
|
||||||
$plans = array_filter(
|
|
||||||
$this->existingPlans,
|
|
||||||
function (Plan $plan) use ($user) {
|
|
||||||
return $plan->getUser()->getId() === $user->getId();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return array_map(
|
|
||||||
function (Plan $plan) {
|
|
||||||
return new Plan(
|
|
||||||
id: $plan->getId(),
|
|
||||||
name: $plan->getName(),
|
|
||||||
user: $plan->getUser(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
$plans
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ namespace Tests\Fakes;
|
||||||
use App\ScheduledNode\CreateScheduledNodeDto;
|
use App\ScheduledNode\CreateScheduledNodeDto;
|
||||||
use App\ScheduledNode\ScheduledNode;
|
use App\ScheduledNode\ScheduledNode;
|
||||||
use App\ScheduledNode\ScheduledNodeRepository;
|
use App\ScheduledNode\ScheduledNodeRepository;
|
||||||
use App\User\User;
|
|
||||||
|
|
||||||
class FakeScheduledNodeRepository implements ScheduledNodeRepository
|
class FakeScheduledNodeRepository implements ScheduledNodeRepository
|
||||||
{
|
{
|
||||||
|
|
@ -21,27 +20,12 @@ class FakeScheduledNodeRepository implements ScheduledNodeRepository
|
||||||
id: $id,
|
id: $id,
|
||||||
date: $dto->date,
|
date: $dto->date,
|
||||||
plan: $dto->plan,
|
plan: $dto->plan,
|
||||||
node: $dto->node,
|
|
||||||
completed: false,
|
|
||||||
);
|
);
|
||||||
$this->existingScheduledNodes[$id] = $scheduledNode;
|
$this->existingScheduledNodes[$id] = $scheduledNode;
|
||||||
|
|
||||||
return $scheduledNode;
|
return $scheduledNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(ScheduledNode $node): ScheduledNode
|
|
||||||
{
|
|
||||||
$this->existingScheduledNodes[$node->getId()] = $node;
|
|
||||||
|
|
||||||
return new ScheduledNode(
|
|
||||||
id: $node->getId(),
|
|
||||||
date: $node->getDate(),
|
|
||||||
plan: $node->getPlan(),
|
|
||||||
node: $node->getNode(),
|
|
||||||
completed: $node->getCompleted()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function find(int $id): ?ScheduledNode
|
public function find(int $id): ?ScheduledNode
|
||||||
{
|
{
|
||||||
return array_find(
|
return array_find(
|
||||||
|
|
@ -61,27 +45,4 @@ class FakeScheduledNodeRepository implements ScheduledNodeRepository
|
||||||
{
|
{
|
||||||
return count($this->existingScheduledNodes);
|
return count($this->existingScheduledNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findByUser(User $user): array
|
|
||||||
{
|
|
||||||
$scheduledNodes = array_filter(
|
|
||||||
$this->existingScheduledNodes,
|
|
||||||
function (ScheduledNode $node) use ($user) {
|
|
||||||
return $node->getPlan()->getUser()->getId() === $user->getId();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return array_map(
|
|
||||||
function (ScheduledNode $node) {
|
|
||||||
return new ScheduledNode(
|
|
||||||
id: $node->getId(),
|
|
||||||
date: $node->getDate(),
|
|
||||||
plan: $node->getPlan(),
|
|
||||||
node: $node->getNode(),
|
|
||||||
completed: $node->getCompleted(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
$scheduledNodes,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ class CreatePlanTest extends TestCase
|
||||||
$this->createScheduledNode = new CreateScheduledNode(
|
$this->createScheduledNode = new CreateScheduledNode(
|
||||||
scheduledNodeRepo: $this->scheduledNodeRepo,
|
scheduledNodeRepo: $this->scheduledNodeRepo,
|
||||||
planRepo: $this->planRepo,
|
planRepo: $this->planRepo,
|
||||||
nodeRepo: $this->nodeRepo,
|
|
||||||
);
|
);
|
||||||
$this->textRepo->create(new CreateTextDto('testname'));
|
$this->textRepo->create(new CreateTextDto('testname'));
|
||||||
$this->useCase = new CreatePlan(
|
$this->useCase = new CreatePlan(
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,15 @@
|
||||||
namespace Tests\Unit\ScheduledNode\UseCases;
|
namespace Tests\Unit\ScheduledNode\UseCases;
|
||||||
|
|
||||||
use App\Exceptions\BadRequestException;
|
use App\Exceptions\BadRequestException;
|
||||||
use App\Node\CreateNodeDto;
|
|
||||||
use App\Node\Node;
|
|
||||||
use App\Plan\CreatePlanDto;
|
use App\Plan\CreatePlanDto;
|
||||||
use App\Plan\Plan;
|
use App\Plan\Plan;
|
||||||
use App\ScheduledNode\ScheduledNode;
|
use App\ScheduledNode\ScheduledNode;
|
||||||
use App\ScheduledNode\ScheduledNodeRepository;
|
use App\ScheduledNode\ScheduledNodeRepository;
|
||||||
use App\ScheduledNode\UseCases\CreateScheduledNode;
|
use App\ScheduledNode\UseCases\CreateScheduledNode;
|
||||||
use App\ScheduledNode\UseCases\CreateScheduledNodeRequest;
|
use App\ScheduledNode\UseCases\CreateScheduledNodeRequest;
|
||||||
use App\Text\Text;
|
|
||||||
use App\User\User;
|
use App\User\User;
|
||||||
use App\ValueObjects\EmailAddress;
|
use App\ValueObjects\EmailAddress;
|
||||||
use DomainException;
|
use DomainException;
|
||||||
use Tests\Fakes\FakeNodeRepository;
|
|
||||||
use Tests\Fakes\FakePlanRepository;
|
use Tests\Fakes\FakePlanRepository;
|
||||||
use Tests\Fakes\FakeScheduledNodeRepository;
|
use Tests\Fakes\FakeScheduledNodeRepository;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
@ -26,20 +22,12 @@ class CreateScheduledNodeTest extends TestCase
|
||||||
|
|
||||||
private FakePlanRepository $planRepo;
|
private FakePlanRepository $planRepo;
|
||||||
|
|
||||||
private FakeNodeRepository $nodeRepo;
|
|
||||||
|
|
||||||
private CreateScheduledNode $useCase;
|
private CreateScheduledNode $useCase;
|
||||||
|
|
||||||
public function setUp(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
$this->scheduledNodeRepo = new FakeScheduledNodeRepository();
|
$this->scheduledNodeRepo = new FakeScheduledNodeRepository();
|
||||||
$this->planRepo = new FakePlanRepository();
|
$this->planRepo = new FakePlanRepository();
|
||||||
$this->nodeRepo = new FakeNodeRepository();
|
|
||||||
$this->nodeRepo->create(new CreateNodeDto(
|
|
||||||
text: new Text(0, 'text name'),
|
|
||||||
title: 'test node',
|
|
||||||
parentNode: null,
|
|
||||||
));
|
|
||||||
$this->planRepo->create(new CreatePlanDto(
|
$this->planRepo->create(new CreatePlanDto(
|
||||||
name: 'testplan',
|
name: 'testplan',
|
||||||
user: new User(
|
user: new User(
|
||||||
|
|
@ -52,7 +40,6 @@ class CreateScheduledNodeTest extends TestCase
|
||||||
$this->useCase = new CreateScheduledNode(
|
$this->useCase = new CreateScheduledNode(
|
||||||
$this->scheduledNodeRepo,
|
$this->scheduledNodeRepo,
|
||||||
$this->planRepo,
|
$this->planRepo,
|
||||||
$this->nodeRepo,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,7 +49,6 @@ class CreateScheduledNodeTest extends TestCase
|
||||||
new CreateScheduledNodeRequest(
|
new CreateScheduledNodeRequest(
|
||||||
date: '2025-01-01',
|
date: '2025-01-01',
|
||||||
planId: 0,
|
planId: 0,
|
||||||
nodeId: 0,
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$this->assertInstanceOf(ScheduledNode::class, $scheduledNode);
|
$this->assertInstanceOf(ScheduledNode::class, $scheduledNode);
|
||||||
|
|
@ -78,24 +64,11 @@ class CreateScheduledNodeTest extends TestCase
|
||||||
new CreateScheduledNodeRequest(
|
new CreateScheduledNodeRequest(
|
||||||
date: '2025-01-01',
|
date: '2025-01-01',
|
||||||
planId: 0,
|
planId: 0,
|
||||||
nodeId: 0
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$this->assertInstanceOf(Plan::class, $scheduledNode->getPlan());
|
$this->assertInstanceOf(Plan::class, $scheduledNode->getPlan());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_scheduled_node_belongs_to_node(): void
|
|
||||||
{
|
|
||||||
$scheduledNode = $this->useCase->execute(
|
|
||||||
new CreateScheduledNodeRequest(
|
|
||||||
date: '2025-01-01',
|
|
||||||
planId: 0,
|
|
||||||
nodeId: 0
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$this->assertInstanceOf(Node::class, $scheduledNode->getNode());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_nonexistant_plan_throws(): void
|
public function test_nonexistant_plan_throws(): void
|
||||||
{
|
{
|
||||||
$this->expectException(DomainException::class);
|
$this->expectException(DomainException::class);
|
||||||
|
|
@ -104,20 +77,6 @@ class CreateScheduledNodeTest extends TestCase
|
||||||
new CreateScheduledNodeRequest(
|
new CreateScheduledNodeRequest(
|
||||||
date: '2025-01-01',
|
date: '2025-01-01',
|
||||||
planId: 1,
|
planId: 1,
|
||||||
nodeId: 0,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_nonexistant_node_throws(): void
|
|
||||||
{
|
|
||||||
$this->expectException(DomainException::class);
|
|
||||||
$this->expectExceptionMessage('Node with id: 1 doesnt exist');
|
|
||||||
$this->useCase->execute(
|
|
||||||
new CreateScheduledNodeRequest(
|
|
||||||
date: '2025-01-01',
|
|
||||||
planId: 0,
|
|
||||||
nodeId: 1,
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +90,6 @@ class CreateScheduledNodeTest extends TestCase
|
||||||
new CreateScheduledNodeRequest(
|
new CreateScheduledNodeRequest(
|
||||||
date: null,
|
date: null,
|
||||||
planId: 0,
|
planId: 0,
|
||||||
nodeId: 0
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -145,21 +103,6 @@ class CreateScheduledNodeTest extends TestCase
|
||||||
new CreateScheduledNodeRequest(
|
new CreateScheduledNodeRequest(
|
||||||
date: '2025-01-01',
|
date: '2025-01-01',
|
||||||
planId: null,
|
planId: null,
|
||||||
nodeId: 0,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_throws_if_node_id_is_null(): void
|
|
||||||
{
|
|
||||||
$this->expectException(BadRequestException::class);
|
|
||||||
$this->expectExceptionMessage('nodeId is required');
|
|
||||||
|
|
||||||
$this->useCase->execute(
|
|
||||||
new CreateScheduledNodeRequest(
|
|
||||||
date: '2025-01-01',
|
|
||||||
planId: 0,
|
|
||||||
nodeId: null,
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,222 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit\ScheduledNode\UseCases;
|
|
||||||
|
|
||||||
use App\Exceptions\BadRequestException;
|
|
||||||
use App\Node\Node;
|
|
||||||
use App\Plan\CreatePlanDto;
|
|
||||||
use App\ScheduledNode\CreateScheduledNodeDto;
|
|
||||||
use App\ScheduledNode\ScheduledNode;
|
|
||||||
use App\ScheduledNode\UseCases\GetTodaysSchedule;
|
|
||||||
use App\ScheduledNode\UseCases\GetTodaysScheduleRequest;
|
|
||||||
use App\Text\Text;
|
|
||||||
use App\User\UseCases\CreateUserDto;
|
|
||||||
use App\ValueObjects\EmailAddress;
|
|
||||||
use DateTimeImmutable;
|
|
||||||
use DomainException;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Tests\Fakes\FakePlanRepository;
|
|
||||||
use Tests\Fakes\FakeScheduledNodeRepository;
|
|
||||||
use Tests\Fakes\FakeUserRepository;
|
|
||||||
|
|
||||||
class GetTodaysScheduleTest extends TestCase
|
|
||||||
{
|
|
||||||
private FakeUserRepository $userRepo;
|
|
||||||
|
|
||||||
private FakePlanRepository $planRepo;
|
|
||||||
|
|
||||||
private FakeScheduledNodeRepository $scheduledNodeRepo;
|
|
||||||
|
|
||||||
private GetTodaysSchedule $useCase;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
$this->userRepo = new FakeUserRepository();
|
|
||||||
$this->scheduledNodeRepo = new FakeScheduledNodeRepository();
|
|
||||||
$this->planRepo = new FakePlanRepository();
|
|
||||||
$user = $this->userRepo->create(new CreateUserDto(
|
|
||||||
email: new EmailAddress('email@email.com'),
|
|
||||||
passwordHash: 'hash',
|
|
||||||
isAdmin: false,
|
|
||||||
));
|
|
||||||
$plan = $this->planRepo->create(new CreatePlanDto(
|
|
||||||
name: 'test plan',
|
|
||||||
user: $user,
|
|
||||||
));
|
|
||||||
$this->scheduledNodeRepo->create(new CreateScheduledNodeDto(
|
|
||||||
date: new DateTimeImmutable('2025-01-02'),
|
|
||||||
plan: $plan,
|
|
||||||
node: new Node(
|
|
||||||
id: 0,
|
|
||||||
title: 'test node',
|
|
||||||
text: new Text(id: 0, name: 'test text'),
|
|
||||||
parentNode: null,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
|
|
||||||
$this->useCase = new GetTodaysSchedule(
|
|
||||||
userRepo: $this->userRepo,
|
|
||||||
scheduledNodeRepo: $this->scheduledNodeRepo,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_returns_array_of_scheduled_nodes(): void
|
|
||||||
{
|
|
||||||
$result = $this->useCase->execute(new GetTodaysScheduleRequest(
|
|
||||||
date: '2025-01-02',
|
|
||||||
userId: 0,
|
|
||||||
));
|
|
||||||
|
|
||||||
$this->assertIsArray($result);
|
|
||||||
$this->assertInstanceOf(ScheduledNode::class, $result[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_returns_all_unfinished_scheduled_nodes_up_until_today(): void
|
|
||||||
{
|
|
||||||
$this->scheduledNodeRepo->create(new CreateScheduledNodeDto(
|
|
||||||
date: new DateTimeImmutable('2025-01-01'),
|
|
||||||
plan: $this->planRepo->find(0),
|
|
||||||
node: new Node(
|
|
||||||
id: 0,
|
|
||||||
title: 'test node',
|
|
||||||
text: new Text(id: 0, name: 'test text'),
|
|
||||||
parentNode: null,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
$result = $this->useCase->execute(new GetTodaysScheduleRequest(
|
|
||||||
date: '2025-01-02',
|
|
||||||
userId: 0,
|
|
||||||
));
|
|
||||||
$this->assertEquals(2, count($result));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_only_returns_uncompleted_nodes(): void
|
|
||||||
{
|
|
||||||
$node = $this->scheduledNodeRepo->create(
|
|
||||||
new CreateScheduledNodeDto(
|
|
||||||
date: new DateTimeImmutable('2025-01-01'),
|
|
||||||
plan: $this->planRepo->find(0),
|
|
||||||
node: new Node(
|
|
||||||
id: 0,
|
|
||||||
title: 'test node',
|
|
||||||
text: new Text(id: 0, name: 'test text'),
|
|
||||||
parentNode: null,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$node->setCompleted(true);
|
|
||||||
$this->scheduledNodeRepo->update($node);
|
|
||||||
|
|
||||||
$result = $this->useCase->execute(new GetTodaysScheduleRequest(
|
|
||||||
date: '2025-01-02',
|
|
||||||
userId: 0,
|
|
||||||
));
|
|
||||||
$this->assertEquals(1, count($result));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_throws_if_date_is_null(): void
|
|
||||||
{
|
|
||||||
$this->expectException(BadRequestException::class);
|
|
||||||
$this->expectExceptionMessage('date is required');
|
|
||||||
|
|
||||||
$this->useCase->execute(new GetTodaysScheduleRequest(
|
|
||||||
date: null,
|
|
||||||
userId: 0,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_throws_if_user_id_is_null(): void
|
|
||||||
{
|
|
||||||
$this->expectException(BadRequestException::class);
|
|
||||||
$this->expectExceptionMessage('userId is required');
|
|
||||||
|
|
||||||
$this->useCase->execute(new GetTodaysScheduleRequest(
|
|
||||||
date: '2025-01-02',
|
|
||||||
userId: null,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_nonexistant_user_throws(): void
|
|
||||||
{
|
|
||||||
$this->expectException(DomainException::class);
|
|
||||||
$this->expectExceptionMessage('User with id: 99 doesnt exist');
|
|
||||||
|
|
||||||
$this->useCase->execute(new GetTodaysScheduleRequest(
|
|
||||||
date: '2025-01-02',
|
|
||||||
userId: 99,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_returns_empty_array_when_user_has_no_scheduled_nodes(): void
|
|
||||||
{
|
|
||||||
$otherUser = $this->userRepo->create(new CreateUserDto(
|
|
||||||
email: new EmailAddress('other@email.com'),
|
|
||||||
passwordHash: 'hash',
|
|
||||||
isAdmin: false,
|
|
||||||
));
|
|
||||||
|
|
||||||
$result = $this->useCase->execute(new GetTodaysScheduleRequest(
|
|
||||||
date: '2025-01-02',
|
|
||||||
userId: $otherUser->getId(),
|
|
||||||
));
|
|
||||||
|
|
||||||
$this->assertIsArray($result);
|
|
||||||
$this->assertEquals(0, count($result));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_excludes_scheduled_nodes_dated_after_today(): void
|
|
||||||
{
|
|
||||||
$this->scheduledNodeRepo->create(new CreateScheduledNodeDto(
|
|
||||||
date: new DateTimeImmutable('2025-01-05'),
|
|
||||||
plan: $this->planRepo->find(0),
|
|
||||||
node: new Node(
|
|
||||||
id: 0,
|
|
||||||
title: 'future node',
|
|
||||||
text: new Text(id: 0, name: 'test text'),
|
|
||||||
parentNode: null,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
|
|
||||||
$result = $this->useCase->execute(new GetTodaysScheduleRequest(
|
|
||||||
date: '2025-01-02',
|
|
||||||
userId: 0,
|
|
||||||
));
|
|
||||||
|
|
||||||
$this->assertEquals(1, count($result));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_does_not_return_other_users_scheduled_nodes(): void
|
|
||||||
{
|
|
||||||
$otherUser = $this->userRepo->create(new CreateUserDto(
|
|
||||||
email: new EmailAddress('other@email.com'),
|
|
||||||
passwordHash: 'hash',
|
|
||||||
isAdmin: false,
|
|
||||||
));
|
|
||||||
$otherPlan = $this->planRepo->create(new CreatePlanDto(
|
|
||||||
name: 'other plan',
|
|
||||||
user: $otherUser,
|
|
||||||
));
|
|
||||||
$this->scheduledNodeRepo->create(new CreateScheduledNodeDto(
|
|
||||||
date: new DateTimeImmutable('2025-01-02'),
|
|
||||||
plan: $otherPlan,
|
|
||||||
node: new Node(
|
|
||||||
id: 0,
|
|
||||||
title: 'other node',
|
|
||||||
text: new Text(id: 0, name: 'test text'),
|
|
||||||
parentNode: null,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
|
|
||||||
$result = $this->useCase->execute(new GetTodaysScheduleRequest(
|
|
||||||
date: '2025-01-02',
|
|
||||||
userId: 0,
|
|
||||||
));
|
|
||||||
|
|
||||||
$this->assertEquals(1, count($result));
|
|
||||||
$resultNode = array_values($result)[0];
|
|
||||||
$this->assertEquals(
|
|
||||||
0,
|
|
||||||
$resultNode->getPlan()->getUser()->getId()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -55,7 +55,6 @@ class PlanControllerTest extends TestCase
|
||||||
$createScheduledNode = new CreateScheduledNode(
|
$createScheduledNode = new CreateScheduledNode(
|
||||||
scheduledNodeRepo: $this->scheduledNodeRepo,
|
scheduledNodeRepo: $this->scheduledNodeRepo,
|
||||||
planRepo: $this->planRepo,
|
planRepo: $this->planRepo,
|
||||||
nodeRepo: $this->nodeRepo,
|
|
||||||
);
|
);
|
||||||
$this->createPlan = new CreatePlan(
|
$this->createPlan = new CreatePlan(
|
||||||
$this->planRepo,
|
$this->planRepo,
|
||||||
|
|
|
||||||
|
|
@ -1,252 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\e2e\Controllers;
|
|
||||||
|
|
||||||
use App\Node\Node;
|
|
||||||
use App\Plan\CreatePlanDto;
|
|
||||||
use App\ScheduledNode\CreateScheduledNodeDto;
|
|
||||||
use App\ScheduledNode\ScheduledNodeController;
|
|
||||||
use App\ScheduledNode\UseCases\GetTodaysSchedule;
|
|
||||||
use App\Text\Text;
|
|
||||||
use App\User\UseCases\CreateUserDto;
|
|
||||||
use App\User\User;
|
|
||||||
use App\ValueObjects\EmailAddress;
|
|
||||||
use DateTimeImmutable;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
use Slim\Psr7\Factory\ServerRequestFactory;
|
|
||||||
use Slim\Psr7\Response;
|
|
||||||
use Tests\Fakes\FakePlanRepository;
|
|
||||||
use Tests\Fakes\FakeScheduledNodeRepository;
|
|
||||||
use Tests\Fakes\FakeUserRepository;
|
|
||||||
|
|
||||||
class ScheduledNodeControllerTest extends TestCase
|
|
||||||
{
|
|
||||||
private FakeUserRepository $userRepo;
|
|
||||||
private FakePlanRepository $planRepo;
|
|
||||||
private FakeScheduledNodeRepository $scheduledNodeRepo;
|
|
||||||
private GetTodaysSchedule $getTodaysSchedule;
|
|
||||||
private ScheduledNodeController $controller;
|
|
||||||
private User $user;
|
|
||||||
|
|
||||||
public function setUp(): void
|
|
||||||
{
|
|
||||||
$this->userRepo = new FakeUserRepository();
|
|
||||||
$this->planRepo = new FakePlanRepository();
|
|
||||||
$this->scheduledNodeRepo = new FakeScheduledNodeRepository();
|
|
||||||
|
|
||||||
$this->user = $this->userRepo->create(new CreateUserDto(
|
|
||||||
email: new EmailAddress('test@test.com'),
|
|
||||||
passwordHash: '',
|
|
||||||
isAdmin: false,
|
|
||||||
));
|
|
||||||
|
|
||||||
$this->getTodaysSchedule = new GetTodaysSchedule(
|
|
||||||
userRepo: $this->userRepo,
|
|
||||||
scheduledNodeRepo: $this->scheduledNodeRepo,
|
|
||||||
);
|
|
||||||
$this->controller = new ScheduledNodeController();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function makeRequest(
|
|
||||||
?string $date,
|
|
||||||
?User $user,
|
|
||||||
): ServerRequestInterface {
|
|
||||||
$request = new ServerRequestFactory()
|
|
||||||
->createServerRequest(
|
|
||||||
'GET',
|
|
||||||
'http://localhost/api/scheduled-nodes'
|
|
||||||
);
|
|
||||||
if ($user !== null) {
|
|
||||||
$request = $request->withAttribute('user', $user);
|
|
||||||
}
|
|
||||||
if ($date !== null) {
|
|
||||||
$request = $request->withQueryParams(['date' => $date]);
|
|
||||||
}
|
|
||||||
return $request;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function seedScheduledNode(
|
|
||||||
User $user,
|
|
||||||
string $date,
|
|
||||||
string $planName,
|
|
||||||
string $nodeTitle,
|
|
||||||
): void {
|
|
||||||
$plan = $this->planRepo->create(new CreatePlanDto(
|
|
||||||
name: $planName,
|
|
||||||
user: $user,
|
|
||||||
));
|
|
||||||
$this->scheduledNodeRepo->create(new CreateScheduledNodeDto(
|
|
||||||
date: new DateTimeImmutable($date),
|
|
||||||
plan: $plan,
|
|
||||||
node: new Node(
|
|
||||||
id: 0,
|
|
||||||
title: $nodeTitle,
|
|
||||||
text: new Text(id: 0, name: 'test text'),
|
|
||||||
parentNode: null,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_returns_200_with_scheduled_nodes_for_user(): void
|
|
||||||
{
|
|
||||||
$this->seedScheduledNode(
|
|
||||||
$this->user,
|
|
||||||
'2025-01-02',
|
|
||||||
'My reading plan',
|
|
||||||
'Bereishis',
|
|
||||||
);
|
|
||||||
|
|
||||||
$response = $this->controller->getScheduledNodes(
|
|
||||||
$this->makeRequest('2025-01-02', $this->user),
|
|
||||||
new Response(),
|
|
||||||
$this->getTodaysSchedule,
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertEquals(200, $response->getStatusCode());
|
|
||||||
$body = json_decode((string) $response->getBody(), true);
|
|
||||||
$this->assertIsArray($body);
|
|
||||||
$this->assertCount(1, $body);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_response_has_expected_fields(): void
|
|
||||||
{
|
|
||||||
$this->seedScheduledNode(
|
|
||||||
$this->user,
|
|
||||||
'2025-01-02',
|
|
||||||
'My reading plan',
|
|
||||||
'Bereishis',
|
|
||||||
);
|
|
||||||
|
|
||||||
$response = $this->controller->getScheduledNodes(
|
|
||||||
$this->makeRequest('2025-01-02', $this->user),
|
|
||||||
new Response(),
|
|
||||||
$this->getTodaysSchedule,
|
|
||||||
);
|
|
||||||
|
|
||||||
$body = json_decode((string) $response->getBody(), true);
|
|
||||||
$this->assertArrayHasKey('id', $body[0]);
|
|
||||||
$this->assertArrayHasKey('date', $body[0]);
|
|
||||||
$this->assertEquals('2025-01-02', $body[0]['date']);
|
|
||||||
$this->assertEquals('My reading plan', $body[0]['planName']);
|
|
||||||
$this->assertEquals('Bereishis', $body[0]['nodeTitle']);
|
|
||||||
$this->assertEquals(false, $body[0]['completed']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_returns_401_when_no_user_attribute(): void
|
|
||||||
{
|
|
||||||
$response = $this->controller->getScheduledNodes(
|
|
||||||
$this->makeRequest('2025-01-02', null),
|
|
||||||
new Response(),
|
|
||||||
$this->getTodaysSchedule,
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertEquals(401, $response->getStatusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_returns_400_when_date_query_param_missing(): void
|
|
||||||
{
|
|
||||||
$response = $this->controller->getScheduledNodes(
|
|
||||||
$this->makeRequest(null, $this->user),
|
|
||||||
new Response(),
|
|
||||||
$this->getTodaysSchedule,
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertEquals(400, $response->getStatusCode());
|
|
||||||
$body = json_decode((string) $response->getBody(), true);
|
|
||||||
$this->assertArrayHasKey('error', $body);
|
|
||||||
$this->assertEquals('date is required', $body['error']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_returns_400_when_date_query_param_empty_string(): void
|
|
||||||
{
|
|
||||||
$response = $this->controller->getScheduledNodes(
|
|
||||||
$this->makeRequest('', $this->user),
|
|
||||||
new Response(),
|
|
||||||
$this->getTodaysSchedule,
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertEquals(400, $response->getStatusCode());
|
|
||||||
$body = json_decode((string) $response->getBody(), true);
|
|
||||||
$this->assertArrayHasKey('error', $body);
|
|
||||||
$this->assertEquals('date is required', $body['error']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_excludes_future_scheduled_nodes(): void
|
|
||||||
{
|
|
||||||
$this->seedScheduledNode(
|
|
||||||
$this->user,
|
|
||||||
'2025-01-10',
|
|
||||||
'My reading plan',
|
|
||||||
'Bereishis',
|
|
||||||
);
|
|
||||||
|
|
||||||
$response = $this->controller->getScheduledNodes(
|
|
||||||
$this->makeRequest('2025-01-02', $this->user),
|
|
||||||
new Response(),
|
|
||||||
$this->getTodaysSchedule,
|
|
||||||
);
|
|
||||||
|
|
||||||
$body = json_decode((string) $response->getBody(), true);
|
|
||||||
$this->assertCount(0, $body);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_excludes_completed_scheduled_nodes(): void
|
|
||||||
{
|
|
||||||
$this->seedScheduledNode(
|
|
||||||
$this->user,
|
|
||||||
'2025-01-02',
|
|
||||||
'My reading plan',
|
|
||||||
'Bereishis',
|
|
||||||
);
|
|
||||||
$stored = $this->scheduledNodeRepo->find(0);
|
|
||||||
$stored->setCompleted(true);
|
|
||||||
$this->scheduledNodeRepo->update($stored);
|
|
||||||
|
|
||||||
$response = $this->controller->getScheduledNodes(
|
|
||||||
$this->makeRequest('2025-01-02', $this->user),
|
|
||||||
new Response(),
|
|
||||||
$this->getTodaysSchedule,
|
|
||||||
);
|
|
||||||
|
|
||||||
$body = json_decode((string) $response->getBody(), true);
|
|
||||||
$this->assertCount(0, $body);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_returns_empty_array_when_user_has_no_nodes(): void
|
|
||||||
{
|
|
||||||
$response = $this->controller->getScheduledNodes(
|
|
||||||
$this->makeRequest('2025-01-02', $this->user),
|
|
||||||
new Response(),
|
|
||||||
$this->getTodaysSchedule,
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertEquals(200, $response->getStatusCode());
|
|
||||||
$body = json_decode((string) $response->getBody(), true);
|
|
||||||
$this->assertEquals([], $body);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_returns_404_when_use_case_throws_domain_exception(): void
|
|
||||||
{
|
|
||||||
$unknownUser = new User(
|
|
||||||
id: 999,
|
|
||||||
email: new EmailAddress('ghost@test.com'),
|
|
||||||
passwordHash: '',
|
|
||||||
isAdmin: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
$response = $this->controller->getScheduledNodes(
|
|
||||||
$this->makeRequest('2025-01-02', $unknownUser),
|
|
||||||
new Response(),
|
|
||||||
$this->getTodaysSchedule,
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertEquals(404, $response->getStatusCode());
|
|
||||||
$body = json_decode((string) $response->getBody(), true);
|
|
||||||
$this->assertArrayHasKey('error', $body);
|
|
||||||
$this->assertEquals(
|
|
||||||
'User with id: 999 doesnt exist',
|
|
||||||
$body['error']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
<body>
|
<body>
|
||||||
<h1>Home</h1>
|
<h1>Home</h1>
|
||||||
<button id="logout">Logout</button>
|
<button id="logout">Logout</button>
|
||||||
<a href="/today">Today's schedule</a>
|
|
||||||
<ul id="texts-list">
|
<ul id="texts-list">
|
||||||
</ul>
|
</ul>
|
||||||
<div id="create-plan-modal" hidden>
|
<div id="create-plan-modal" hidden>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Daily Goals - Today</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Today</h1>
|
|
||||||
<ul id="scheduled-nodes-list">
|
|
||||||
</ul>
|
|
||||||
<script src="/js/auth.js"></script>
|
|
||||||
<script src="/js/today.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue