diff --git a/CODING_STANDARDS.md b/CODING_STANDARDS.md index c07b37b..30a3c27 100644 --- a/CODING_STANDARDS.md +++ b/CODING_STANDARDS.md @@ -94,3 +94,7 @@ These rules apply to every file in this repo , for humans and agents alike. 4. **Needless complexity**. 5. **Needless repetition**. 6. **Opacity** — the code is hard to understand. + +## Credits + +Thanks to [@wojtekluwojteklu/clean_code.md](https://gist.github.com/wojteklu/73c6914cc446146b8b533c0988cf8d29) diff --git a/README.md b/README.md index c2e713a..7fd8294 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Data is stored in `./data/` by default. For a real server (systemd, HTTPS, backu ### Develop -Requires Go 1.25+, Node.js 22, [Bun](https://bun.sh) and [Task](https://taskfile.dev). +Requires Go 1.27+, Node.js 22, [Bun](https://bun.sh) and [Task](https://taskfile.dev). ```sh cp .env.example .env diff --git a/Taskfile.yml b/Taskfile.yml index d9f56e6..b917c24 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -57,7 +57,7 @@ tasks: desc: Gremlins mutation testing of the use cases → tmp/gremlins.json cmds: - mkdir -p tmp - - gremlins unleash --integration --tags nofrontend --output tmp/gremlins.json ./internal/infrastructure/useCases + - gremlins unleash --integration --tags nofrontend --coverpkg=./internal/infrastructure/useCases/... --timeout-coefficient 10 --output tmp/gremlins.json ./internal/infrastructure/useCases lint: desc: gofmt check, go vet, eslint (--fix) and vue-tsc diff --git a/docs/development.md b/docs/development.md index 4718f45..b2a7382 100644 --- a/docs/development.md +++ b/docs/development.md @@ -4,7 +4,7 @@ | Tool | Version | Install | |-------------------------------|----------------------------|----------------------------------------------------------| -| [Go](https://go.dev/dl/) | 1.25+ (see `go.mod`) | | +| [Go](https://go.dev/dl/) | 1.27+ (see `go.mod`) | | | [Node.js](https://nodejs.org) | 20.19+ or 22.12+ (22 LTS recommended) | `nvm install 22` — Vite and vue-tsc run on Node | | [Bun](https://bun.sh) (package manager and script runner) | 1.x | `powershell -c "irm bun.sh/install.ps1 \| iex"` or `curl -fsSL https://bun.sh/install \| bash` | | [Task](https://taskfile.dev) | 3.43+ | `go install github.com/go-task/task/v3/cmd/task@latest` | diff --git a/features/use_case_baseline.feature b/features/use_case_baseline.feature new file mode 100644 index 0000000..883e286 --- /dev/null +++ b/features/use_case_baseline.feature @@ -0,0 +1,208 @@ +Feature: Use case baseline characterization + + As the maintainer of RenewCMS + I want every use case in internal/infrastructure/useCases pinned down by tests + So that a refactor cannot silently change behaviour that callers already depend on + + Background: + Given the article, image, user and mail repository fakes in test/ each implement their domain Repository interface in full, with no stubbed method returning a hardcoded zero value + And each fake mirrors the observable behaviour of the matching real repository in internal/infrastructure/persistence/repositories or internal/infrastructure/mailer, including which operations return a not-found style error for an unknown id and which succeed silently instead + And each fake can be configured to fail on its next call to a chosen method, independently of the state it already holds, returning a generic repository error + + Scenario Outline: CreateArticle enforces the 3-200 character title length limit + Given an empty article repository fake + When CreateArticle is called with a title that is characters long and a body "Some body text" + Then the result is + + Examples: + | length | outcome | + | 0 | the error article.ErrInvalidTitle, and no article is stored | + | 2 | the error article.ErrInvalidTitle, and no article is stored | + | 3 | a created article whose id is repository-assigned and is_online is false | + | 200 | a created article whose id is repository-assigned and is_online is false | + | 201 | the error article.ErrInvalidTitle, and no article is stored | + + Scenario: CreateArticle propagates a configured repository failure + Given an article repository fake configured to fail on Create + When CreateArticle is called with a valid title "Backyard Renovation Update" and a body "Some body text" + Then the use case returns the repository's error unchanged + And no article is stored in the fake + + Scenario Outline: GetArticle and GetOnlineArticle expose different behaviour for a missing article than for an offline one + Given an article repository fake containing: + | id | title | is_online | + | 1 | Online Article | true | + | 2 | Offline Article | false | + When is called with id + Then the result is + + Examples: + | method | id | outcome | + | GetArticle | 1 | the article with id 1 | + | GetArticle | 2 | the article with id 2 (GetArticle does not hide offline articles) | + | GetArticle | 9 | the repository's not-found error (gorm.ErrRecordNotFound) | + | GetOnlineArticle | 1 | the article with id 1 | + | GetOnlineArticle | 2 | the error article.ErrArticleNotFound | + | GetOnlineArticle | 9 | the repository's not-found error (gorm.ErrRecordNotFound), not article.ErrArticleNotFound | + + Scenario Outline: GetArticleByName delegates verbatim to the repository + Given an article repository fake containing an article titled "Backyard Renovation Update" with id 1 + When GetArticleByName is called with "" + Then the result is + + Examples: + | name | outcome | + | Backyard Renovation Update | the article with id 1 | + | Unknown Title | the repository's not-found error (gorm.ErrRecordNotFound) | + + Scenario Outline: ListArticles filters by is_online + Given an article repository fake containing: + | id | title | is_online | + | 1 | A | true | + | 2 | B | false | + | 3 | C | true | + When is called + Then the result is the articles with ids , and no error + + Examples: + | method | ids | + | ListOnlineArticles | 1,3 | + | ListAllArticles | 1,2,3 | + + Scenario: Listing articles on an empty repository returns an empty slice, not an error + Given an empty article repository fake + When ListOnlineArticles is called + Then the result is an empty slice and no error + + Scenario Outline: UpdateArticle delegates each operation to the repository and surfaces not-found for an unknown id + Given an article repository fake containing an article with id 1 and an image with id 1 + When is called with + Then the result is + + Examples: + | method | input | outcome | + | UpdateBody | id 1, body "New body" | the article with id 1 and body "New body" | + | UpdateBody | id 9, body "New body" | the repository's not-found error (gorm.ErrRecordNotFound) | + | UpdateIsOnline | id 1, isOnline true | the article with id 1 and is_online true | + | UpdateIsOnline | id 9, isOnline true | the repository's not-found error (gorm.ErrRecordNotFound) | + | AddImage | articleId 1, imageId 1 | no error, and the image is attached to the article | + | AddImage | articleId 9, imageId 1 | the repository's not-found error (gorm.ErrRecordNotFound) | + | AddImage | articleId 1, imageId 9 | the repository's not-found error (gorm.ErrRecordNotFound) | + + Scenario Outline: DeleteArticle succeeds silently whether or not the id exists + Given an article repository fake containing an article with id 1 + When DeleteArticle is called with id + Then no error is returned + + Examples: + | id | + | 1 | + | 9 | + + Scenario: DeleteArticle propagates a configured repository failure + Given an article repository fake configured to fail on Delete + When DeleteArticle is called with id 1 + Then the use case returns the repository's error unchanged + + Scenario Outline: CreateImage delegates verbatim to the repository, including its content-type check + Given an empty image repository fake that mirrors the real repository's accepted content types image/png, image/jpeg, image/webp and image/svg+xml + When CreateImage is called with content-type "" + Then the result is + + Examples: + | content_type | outcome | + | image/png | a created image whose id is repository-assigned | + | image/jpeg | a created image whose id is repository-assigned | + | image/webp | a created image whose id is repository-assigned | + | image/svg+xml | a created image whose id is repository-assigned | + | image/gif | an unsupported-content-type error, and no image is stored | + + Scenario: CreateImage propagates a configured repository failure + Given an image repository fake configured to fail on Create + When CreateImage is called with a valid image/png input + Then the use case returns the repository's error unchanged + + Scenario Outline: DeleteImage requires the image to exist, unlike article and user deletion + Given an image repository fake containing an image with id 1 + When DeleteImage is called with id + Then the result is + + Examples: + | id | outcome | + | 1 | no error, and the image is removed from the fake | + | 9 | the repository's not-found error (gorm.ErrRecordNotFound) | + + Scenario: CreateUser assigns a fresh verification code and starts unverified + Given an empty user repository fake + When CreateUser is called with username "alice", password "hunter2example" and email "alice@example.com" + Then the result is a created user whose id is repository-assigned, is_verified is false, verification_code is a newly generated value distinct from any previous call, verification_expiration is about 2 hours from now, and password_reset_code is empty + + Scenario: CreateUser propagates a configured repository failure + Given a user repository fake configured to fail on Create + When CreateUser is called with a valid username, password and email + Then the use case returns the repository's error unchanged + + Scenario Outline: GetUser reads by id, username or email and surfaces not-found + Given a user repository fake containing a user with id 1, username "alice" and email "alice@example.com" + When is called with "" + Then the result is + + Examples: + | method | input | outcome | + | GetUser | 1 | the user with id 1 | + | GetUser | 9 | the repository's not-found error (gorm.ErrRecordNotFound) | + | GetUserByUsername | alice | the user with id 1 | + | GetUserByUsername | bob | the repository's not-found error (gorm.ErrRecordNotFound) | + | GetUserByEmail | alice@example.com | the user with id 1 | + | GetUserByEmail | bob@example.com | the repository's not-found error (gorm.ErrRecordNotFound) | + + Scenario: ListUsers returns every user in the fake + Given a user repository fake containing users with ids 1 and 2 + When ListUsers is called + Then the result is the users with ids 1 and 2, and no error + + Scenario Outline: UpdateUser delegates each operation to the repository and surfaces not-found for an unknown id + Given a user repository fake containing a user with id 1 + When is called with id and + Then the result is + + Examples: + | method | id | input | outcome | + | UpdateVerificationStatus | 1 | isVerified true | the user with id 1 and is_verified true | + | UpdateVerificationStatus | 9 | isVerified true | the repository's not-found error (gorm.ErrRecordNotFound) | + | UpdatePassword | 1 | password "newpassword123" | the user with id 1 | + | UpdatePassword | 9 | password "newpassword123" | the repository's not-found error (gorm.ErrRecordNotFound) | + | UpdatePasswordResetCode | 1 | code "reset-code-123" | the user with id 1 | + | UpdatePasswordResetCode | 9 | code "reset-code-123" | the repository's not-found error (gorm.ErrRecordNotFound) | + + Scenario Outline: DeleteUser succeeds silently whether or not the id exists + Given a user repository fake containing a user with id 1 + When DeleteUser is called with id + Then no error is returned + + Examples: + | id | + | 1 | + | 9 | + + Scenario Outline: SendMail forwards its arguments verbatim to the mail repository + Given a mail repository fake + When SendMail is called with receiver "", template "