feat: Authors feature backend (and upgraded deprecated mods)
This commit is contained in:
1 parent
a11338094e
commit
b34a173d83
103 files changed
+6454
-240
No files matched your search
@@ -0,0 +1,863 @@
|
||||
Feature: Authors backend, public bylines that articles credit in order
|
||||
|
||||
As an owner or admin of a RenewCMS installation
|
||||
I want to manage authors and credit them on articles in a chosen order
|
||||
So that readers see who wrote an article, whether or not the writer has an account
|
||||
|
||||
Background:
|
||||
Given the spec of record is docs/design/screens/authors.md, docs/design/screens/editor.md (site and authors), docs/design/screens/profile.md, docs/design/states.md (author rows), docs/design/routes.md and docs/design/sites-authors-slices.md
|
||||
And this slice is backend only: no SPA, no change to docs/design
|
||||
And an Author has id (uint32), name, slug, bio, avatar_id (nullable media id), website (nullable), social_links, user_id (nullable and unique), is_linked and article_count, in snake_case JSON
|
||||
And an article has zero or more authors in a stored order through an ordered article_authors join; the first is the primary byline
|
||||
And zero authors on an article is allowed and never blocks publishing
|
||||
And the routes live under /v1/author, mounted inside the JWT group: GET /, POST /, POST /me, PATCH /{id}, DELETE /{id}?reassign_to={id|none}
|
||||
And GET /v1/author/ accepts a session or an API key from a verified account; every write, POST /me included, is session-only
|
||||
And POST / and DELETE /{id} go through RequireOwnerOrAdmin with its existing body "Only owners and admins can do this."
|
||||
And PATCH /{id} is allowed to owner and admin on any author, and to any verified account on the author linked to itself, and otherwise answers 403 with the body "author_rights"
|
||||
And the only roles defined today are "owner" and "admin", so a role-without-author-rights is exercised with a verified account whose role is any other value
|
||||
And error responses are plain text from http.Error whose body is exactly the code token: author_not_found, author_slug_taken, author_has_articles, user_already_linked, author_already_linked, user_not_found, author_rights, bio_too_long, invalid_url, too_many_links, invalid_slug, invalid_reassign, image_not_found
|
||||
And bio_too_long, invalid_url and too_many_links answer 422; other validate-tag failures answer 400 with the validator error text, a body that cannot be decoded answers 400 with apperr.BodyErrorMessage, and a malformed id answers 400 with apperr.IdUint32ErrorMessage
|
||||
And the name has 1 to 100 characters after trimming, the slug matches ^[a-z0-9]+(-[a-z0-9]+)*$ with 1 to 100 characters, the bio has at most 280 characters counted in runes
|
||||
And website and every social link URL must be https with a host, at most 2048 characters; an empty or null website means none
|
||||
And social_links holds at most 5 items of {"type", "url"} where type is one of x, linkedin, github, mastodon, custom
|
||||
And an author has user_email in its JSON only for owner and admin; it is absent for every other caller
|
||||
And an author inside an article has only id, name, slug, bio, avatar_id, website and social_links
|
||||
And request validation goes through validate tags and validator.Validate.Struct
|
||||
And handlers reach authors only through use cases and domain interfaces, and no comment is added anywhere
|
||||
|
||||
Rule: Reading authors
|
||||
|
||||
Scenario: The list is newest first with article counts
|
||||
Given the authors "Léa Fontaine" (id 1), "Inès Duarte" (id 2) and "Camille Roche" (id 3)
|
||||
And "Inès Duarte" is credited on 9 articles and "Léa Fontaine" on none
|
||||
When a verified account sends GET /v1/author/
|
||||
Then the status is 200
|
||||
And the body is {"items": [...], "total": 3, "page": 1, "size": 25}
|
||||
And the items are "Camille Roche", "Inès Duarte", "Léa Fontaine" in that order
|
||||
And every item carries article_count
|
||||
|
||||
Scenario Outline: Who can read the list
|
||||
Given at least one author exists
|
||||
When GET /v1/author/ is sent with <credentials>
|
||||
Then the status is <status>
|
||||
|
||||
Examples:
|
||||
| credentials | status |
|
||||
| the owner's jwt cookie | 200 |
|
||||
| a verified admin's jwt cookie | 200 |
|
||||
| a verified account with another role | 200 |
|
||||
| a valid API key | 200 |
|
||||
| an unverified account's jwt cookie | 403 |
|
||||
| no credentials | 401 |
|
||||
|
||||
Scenario Outline: Search matches the name or the slug
|
||||
Given the authors "Camille Roche" (slug "camille-roche") and "Zoe Lambert" (slug "zl")
|
||||
When a verified account sends GET /v1/author/?q=<q>
|
||||
Then the items are <names>
|
||||
And total is <total>
|
||||
|
||||
Examples:
|
||||
| q | names | total |
|
||||
| camille | "Camille Roche" | 1 |
|
||||
| ROCHE | "Camille Roche" | 1 |
|
||||
| zl | "Zoe Lambert" | 1 |
|
||||
| zoe | "Zoe Lambert" | 1 |
|
||||
| nobody | none | 0 |
|
||||
|
||||
Scenario Outline: Pagination follows the article list
|
||||
Given 30 authors
|
||||
When a verified account sends GET /v1/author/?<params>
|
||||
Then the status is <status>
|
||||
And <result>
|
||||
|
||||
Examples:
|
||||
| params | status | result |
|
||||
| size=25&page=2 | 200 | 5 items, total 30, page 2, size 25 |
|
||||
| size=5 | 200 | 5 items, total 30, page 1, size 5 |
|
||||
| page=3&size=100 | 200 | 0 items, total 30 |
|
||||
| size=30 | 400 | the body is apperr.BodyErrorMessage |
|
||||
| page=0 | 400 | the body is apperr.BodyErrorMessage |
|
||||
| page=abc | 400 | the body is apperr.BodyErrorMessage |
|
||||
|
||||
Scenario: The account email is shown to owner and admin only
|
||||
Given the author "Inès Duarte" is linked to the user ines.duarte@renew.example
|
||||
When the owner sends GET /v1/author/
|
||||
Then "Inès Duarte" has is_linked true, user_id set and user_email "ines.duarte@renew.example"
|
||||
When a verified account with another role sends GET /v1/author/
|
||||
Then "Inès Duarte" has is_linked true
|
||||
And no "user_email" key appears anywhere in the body
|
||||
And the body does not contain "ines.duarte@renew.example"
|
||||
|
||||
Scenario: A guest author is not linked
|
||||
Given the author "Camille Roche" has no user
|
||||
When a verified account sends GET /v1/author/
|
||||
Then "Camille Roche" has is_linked false, user_id null and no user_email
|
||||
|
||||
Scenario: An author without avatar or website returns nulls
|
||||
When a verified account reads an author with no avatar and no website
|
||||
Then avatar_id is null, website is null, bio is "" and social_links is []
|
||||
|
||||
Rule: Creating an author is owner or admin only
|
||||
|
||||
Scenario Outline: An owner or admin creates an author
|
||||
When <credentials> sends POST /v1/author/ with {"name": "Camille Roche", "slug": "camille-roche", "bio": "Writer.", "website": "https://example.com", "social_links": [{"type": "x", "url": "https://x.com/camille"}]}
|
||||
Then the status is 201
|
||||
And the body is the author with a new id, article_count 0, is_linked false and the given values
|
||||
And the author is stored
|
||||
|
||||
Examples:
|
||||
| credentials |
|
||||
| the owner's jwt cookie |
|
||||
| a verified admin's jwt cookie |
|
||||
|
||||
Scenario: Only the name is required
|
||||
When the owner sends POST /v1/author/ with {"name": "Zoe Lambert"}
|
||||
Then the status is 201
|
||||
And the slug is "zoe-lambert", bio is "", avatar_id, website and user_id are null and social_links is []
|
||||
|
||||
Scenario Outline: The slug is generated from the name
|
||||
When the owner sends POST /v1/author/ with {"name": "<name>"}
|
||||
Then the slug is "<slug>"
|
||||
|
||||
Examples:
|
||||
| name | slug |
|
||||
| Camille Roche | camille-roche |
|
||||
| Inès Duarte | ines-duarte |
|
||||
| Dr. O'Neil Smith | dr-o-neil-smith |
|
||||
| !!! | author |
|
||||
|
||||
Scenario: A generated slug that is taken is refused
|
||||
Given the author with slug "camille-roche" exists
|
||||
When the owner sends POST /v1/author/ with {"name": "Camille Roche"}
|
||||
Then the status is 409
|
||||
And the body is "author_slug_taken"
|
||||
And no author is added
|
||||
|
||||
Scenario: A taken slug is refused
|
||||
Given the author with slug "camille-roche" exists
|
||||
When the owner sends POST /v1/author/ with {"name": "Other", "slug": "camille-roche"}
|
||||
Then the status is 409
|
||||
And the body is "author_slug_taken"
|
||||
And no author is added
|
||||
|
||||
Scenario: The slug is unique even if the repository reports a race
|
||||
Given the repository reports a unique violation on the slug
|
||||
When the owner sends POST /v1/author/ with a free-looking slug
|
||||
Then the status is 409 with body "author_slug_taken"
|
||||
|
||||
Scenario Outline: Fields are validated
|
||||
When the owner sends POST /v1/author/ with <field> <value>
|
||||
Then the status is <status>
|
||||
And the body is "<body>"
|
||||
And no author is added
|
||||
|
||||
Examples:
|
||||
| field | value | status | body |
|
||||
| name | (empty) | 400 | the validator error text |
|
||||
| name | " " | 400 | the validator error text |
|
||||
| name | 101 characters | 400 | the validator error text |
|
||||
| slug | "Camille" | 400 | invalid_slug |
|
||||
| slug | "camille_roche" | 400 | invalid_slug |
|
||||
| slug | "-camille" | 400 | invalid_slug |
|
||||
| slug | 101 characters | 400 | invalid_slug |
|
||||
| bio | 281 characters | 422 | bio_too_long |
|
||||
| website | "http://example.com" | 422 | invalid_url |
|
||||
| website | "example.com" | 422 | invalid_url |
|
||||
| website | "https://" | 422 | invalid_url |
|
||||
| social_links | 6 links | 422 | too_many_links |
|
||||
| social_links | [{"type": "x", "url": "http://x.com/a"}] | 422 | invalid_url |
|
||||
| social_links | [{"type": "facebook", "url": "https://f.com/a"}] | 400 | the validator error text |
|
||||
| avatar_id | 999 (no such image) | 400 | image_not_found |
|
||||
|
||||
Scenario Outline: Boundaries are accepted
|
||||
When the owner sends POST /v1/author/ with <field> <value>
|
||||
Then the status is 201
|
||||
|
||||
Examples:
|
||||
| field | value |
|
||||
| name | 100 characters |
|
||||
| slug | 100 characters |
|
||||
| bio | 280 characters |
|
||||
| bio | 280 multi-byte characters |
|
||||
| social_links | 5 links |
|
||||
| social_links | 2 links of type "custom" |
|
||||
| website | "" |
|
||||
|
||||
Scenario: A malformed body is refused
|
||||
When the owner sends POST /v1/author/ with the body "{not json"
|
||||
Then the status is 400
|
||||
And the body is apperr.BodyErrorMessage
|
||||
And no author is added
|
||||
|
||||
Scenario: The role is checked before the body
|
||||
When an account with another role sends POST /v1/author/ with the body "{not json"
|
||||
Then the status is 403 with body "Only owners and admins can do this."
|
||||
|
||||
Scenario: An author can be created linked to a user
|
||||
Given the user 3 has no author
|
||||
When the owner sends POST /v1/author/ with {"name": "Marta Alves", "user_id": 3}
|
||||
Then the status is 201
|
||||
And the author has user_id 3 and is_linked true
|
||||
|
||||
Scenario Outline: An author cannot be created with an unusable user
|
||||
When the owner sends POST /v1/author/ with {"name": "X", "user_id": <user_id>}
|
||||
Then the status is <status>
|
||||
And the body is "<body>"
|
||||
And no author is added
|
||||
|
||||
Examples:
|
||||
| user_id | status | body |
|
||||
| 999 (no such user) | 404 | user_not_found |
|
||||
| 3 (already has author) | 409 | user_already_linked |
|
||||
|
||||
Rule: The caller creates their own author
|
||||
|
||||
Scenario Outline: The author is named from the display name or the username
|
||||
Given the verified account has display name "<display>" and username "<username>" and no author
|
||||
When it sends POST /v1/author/me
|
||||
Then the status is 201
|
||||
And the author name is "<name>" and the slug is "<slug>"
|
||||
And the author has user_id of the caller and is_linked true
|
||||
|
||||
Examples:
|
||||
| display | username | name | slug |
|
||||
| Marta Alves | marta | Marta Alves | marta-alves |
|
||||
| (empty) | marta | marta | marta |
|
||||
|
||||
Scenario Outline: Any verified role may do it
|
||||
When <credentials> sends POST /v1/author/me
|
||||
Then the status is 201
|
||||
|
||||
Examples:
|
||||
| credentials |
|
||||
| the owner's jwt cookie |
|
||||
| a verified admin's jwt cookie |
|
||||
| a verified account with another role |
|
||||
|
||||
Scenario: A taken slug is de-duplicated silently
|
||||
Given the author with slug "marta-alves" exists and "marta-alves-2" too
|
||||
When "Marta Alves" sends POST /v1/author/me
|
||||
Then the status is 201
|
||||
And the slug is "marta-alves-3"
|
||||
And no error is returned
|
||||
|
||||
Scenario: The suffix still fits in the slug limit
|
||||
Given a display name whose slug is 100 characters and that slug is taken
|
||||
When the caller sends POST /v1/author/me
|
||||
Then the status is 201
|
||||
And the slug is at most 100 characters and ends with "-2"
|
||||
|
||||
Scenario: A caller who already has an author is refused
|
||||
Given the caller is linked to an author
|
||||
When the caller sends POST /v1/author/me
|
||||
Then the status is 409
|
||||
And the body is "user_already_linked"
|
||||
And no author is added
|
||||
|
||||
Scenario: Two concurrent calls create one author
|
||||
Given the repository reports a unique violation on user_id
|
||||
When the caller sends POST /v1/author/me
|
||||
Then the status is 409 with body "user_already_linked"
|
||||
|
||||
Scenario Outline: The endpoint is session-only and verified-only
|
||||
When POST /v1/author/me is sent with <credentials>
|
||||
Then the status is <status>
|
||||
And no author is added
|
||||
|
||||
Examples:
|
||||
| credentials | status | body |
|
||||
| a valid API key | 403 | This action needs a real session. |
|
||||
| an unverified account's jwt cookie | 403 | the existing verification message |
|
||||
| no credentials | 401 | the existing unauthorized message |
|
||||
|
||||
Rule: Editing an author
|
||||
|
||||
Scenario: The owner edits any author
|
||||
Given the author 2 "Camille Roche" with slug "camille-roche"
|
||||
When the owner sends PATCH /v1/author/2 with {"name": "Camille R.", "slug": "camille-r", "bio": "New.", "website": "https://camille.example", "social_links": [{"type": "github", "url": "https://github.com/camille"}]}
|
||||
Then the status is 200
|
||||
And the body is the author with the new values
|
||||
|
||||
Scenario: Absent fields are untouched
|
||||
Given the author 2 has a bio, a website and an avatar
|
||||
When the owner sends PATCH /v1/author/2 with {"name": "Camille R."}
|
||||
Then only the name changed
|
||||
|
||||
Scenario Outline: Nullable fields are cleared
|
||||
Given the author 2 has an avatar 5, a website and 2 social links
|
||||
When the owner sends PATCH /v1/author/2 with <body>
|
||||
Then the status is 200
|
||||
And <result>
|
||||
|
||||
Examples:
|
||||
| body | result |
|
||||
| {"avatar_id": null} | avatar_id is null |
|
||||
| {"website": null} | website is null |
|
||||
| {"website": ""} | website is null |
|
||||
| {"social_links": []} | social_links is [] |
|
||||
| {"bio": ""} | bio is "" |
|
||||
|
||||
Scenario: The avatar is set to an existing image
|
||||
Given the image 5 exists
|
||||
When the owner sends PATCH /v1/author/2 with {"avatar_id": 5}
|
||||
Then the author 2 has avatar_id 5
|
||||
|
||||
Scenario: Social links are replaced as a whole
|
||||
Given the author 2 has 3 social links
|
||||
When the owner sends PATCH /v1/author/2 with {"social_links": [{"type": "x", "url": "https://x.com/a"}]}
|
||||
Then the author 2 has exactly that one link
|
||||
|
||||
Scenario Outline: Editing is validated like creation
|
||||
When the owner sends PATCH /v1/author/2 with <field> <value>
|
||||
Then the status is <status>
|
||||
And the body is "<body>"
|
||||
And the author 2 is unchanged
|
||||
|
||||
Examples:
|
||||
| field | value | status | body |
|
||||
| name | (empty) | 400 | the validator error text |
|
||||
| slug | "Bad Slug" | 400 | invalid_slug |
|
||||
| bio | 281 characters | 422 | bio_too_long |
|
||||
| website | "http://example.com" | 422 | invalid_url |
|
||||
| social_links | 6 links | 422 | too_many_links |
|
||||
| avatar_id | 999 | 400 | image_not_found |
|
||||
|
||||
Scenario: Keeping the own slug is not a collision
|
||||
When the owner sends PATCH /v1/author/2 with {"slug": "camille-roche", "bio": "x"} and the slug is already that author's
|
||||
Then the status is 200
|
||||
|
||||
Scenario: Taking another author's slug is refused
|
||||
Given the author 3 has slug "zoe-lambert"
|
||||
When the owner sends PATCH /v1/author/2 with {"slug": "zoe-lambert"}
|
||||
Then the status is 409
|
||||
And the body is "author_slug_taken"
|
||||
And the author 2 is unchanged
|
||||
|
||||
Scenario Outline: An empty or unknown update is refused
|
||||
When the owner sends PATCH /v1/author/2 with <body>
|
||||
Then the status is 400
|
||||
And the body is apperr.BodyErrorMessage
|
||||
And the author 2 is unchanged
|
||||
|
||||
Examples:
|
||||
| body |
|
||||
| {} |
|
||||
| {"colour": "x"} |
|
||||
| {"name": null} |
|
||||
|
||||
Scenario: A missing author is not found
|
||||
When the owner sends PATCH /v1/author/999 with {"name": "X"}
|
||||
Then the status is 404
|
||||
And the body is "author_not_found"
|
||||
|
||||
Scenario: A malformed id is refused
|
||||
When the owner sends PATCH /v1/author/abc with {"name": "X"}
|
||||
Then the status is 400
|
||||
And the body is apperr.IdUint32ErrorMessage
|
||||
|
||||
Scenario: A no-op edit records nothing
|
||||
When the owner sends PATCH /v1/author/2 with the values it already has
|
||||
Then the status is 200
|
||||
And no activity event is recorded
|
||||
|
||||
Scenario Outline: A role without author rights edits only its own author
|
||||
Given the verified account with another role is linked to the author 4
|
||||
When it sends PATCH /v1/author/<id> with {"bio": "Updated."}
|
||||
Then the status is <status>
|
||||
And the body is "<body>"
|
||||
|
||||
Examples:
|
||||
| id | status | body |
|
||||
| 4 | 200 | the author |
|
||||
| 2 | 403 | author_rights |
|
||||
| 999 | 404 | author_not_found |
|
||||
|
||||
Scenario Outline: Own author edits include name, slug and avatar
|
||||
Given the verified account with another role is linked to the author 4
|
||||
When it sends PATCH /v1/author/4 with <body>
|
||||
Then the status is 200
|
||||
|
||||
Examples:
|
||||
| body |
|
||||
| {"name": "Marta A."} |
|
||||
| {"slug": "marta-a"} |
|
||||
| {"avatar_id": 5} |
|
||||
| {"social_links": []} |
|
||||
|
||||
Scenario: A role without author rights cannot change the link
|
||||
Given the verified account with another role is linked to the author 4
|
||||
When it sends PATCH /v1/author/4 with {"user_id": null}
|
||||
Then the status is 403
|
||||
And the body is "author_rights"
|
||||
And the author 4 is still linked
|
||||
|
||||
Scenario: An API key cannot edit an author
|
||||
When PATCH /v1/author/2 is sent with a valid API key
|
||||
Then the status is 403 with body "This action needs a real session."
|
||||
|
||||
Rule: Linking and unlinking a user
|
||||
|
||||
Scenario: The owner links a user to a guest author
|
||||
Given the author 2 is a guest and the user 3 has no author
|
||||
When the owner sends PATCH /v1/author/2 with {"user_id": 3}
|
||||
Then the status is 200
|
||||
And the author 2 has user_id 3, is_linked true and user_email of the user 3
|
||||
|
||||
Scenario: A user cannot have two authors
|
||||
Given the user 3 is linked to the author 4
|
||||
When the owner sends PATCH /v1/author/2 with {"user_id": 3}
|
||||
Then the status is 409
|
||||
And the body is "user_already_linked"
|
||||
And the author 2 is unchanged
|
||||
|
||||
Scenario: A linked author must be unlinked before it can be relinked
|
||||
Given the author 2 is linked to the user 3 and the user 5 has no author
|
||||
When the owner sends PATCH /v1/author/2 with {"user_id": 5}
|
||||
Then the status is 409
|
||||
And the body is "author_already_linked"
|
||||
|
||||
Scenario: Linking the user it already has is a no-op
|
||||
Given the author 2 is linked to the user 3
|
||||
When the owner sends PATCH /v1/author/2 with {"user_id": 3}
|
||||
Then the status is 200
|
||||
And no activity event is recorded
|
||||
|
||||
Scenario: An unknown user cannot be linked
|
||||
When the owner sends PATCH /v1/author/2 with {"user_id": 999}
|
||||
Then the status is 404
|
||||
And the body is "user_not_found"
|
||||
|
||||
Scenario: The owner unlinks a user
|
||||
Given the author 2 is linked to the user 3
|
||||
When the owner sends PATCH /v1/author/2 with {"user_id": null}
|
||||
Then the status is 200
|
||||
And the author 2 has user_id null, is_linked false and no user_email
|
||||
And the user 3 can create or be linked to an author again
|
||||
And the author 2 stays credited on its articles
|
||||
|
||||
Scenario: Unlinking a guest author is a no-op
|
||||
When the owner sends PATCH /v1/author/2 with {"user_id": null} on a guest author
|
||||
Then the status is 200
|
||||
And no activity event is recorded
|
||||
|
||||
Scenario: A new article by the unlinked user no longer starts with the author
|
||||
Given the user 3 was unlinked from the author 2
|
||||
When the user 3 creates an article
|
||||
Then the article has no authors
|
||||
|
||||
Rule: Deleting an author
|
||||
|
||||
Scenario: An author without articles is deleted
|
||||
Given the author 2 credits no article
|
||||
When the owner sends DELETE /v1/author/2
|
||||
Then the status is 204
|
||||
And the author 2 no longer exists
|
||||
|
||||
Scenario: An author with articles needs a destination
|
||||
Given the author 2 is credited on 3 articles
|
||||
When the owner sends DELETE /v1/author/2
|
||||
Then the status is 409
|
||||
And the body is "author_has_articles"
|
||||
And the author and the 3 credits are unchanged
|
||||
|
||||
Scenario: The articles are reassigned to another author
|
||||
Given the author 2 is credited on the articles 10, 11 and 12 and the author 3 exists
|
||||
When the owner sends DELETE /v1/author/2?reassign_to=3
|
||||
Then the status is 204
|
||||
And the articles 10, 11 and 12 credit the author 3
|
||||
And the author 2 no longer exists
|
||||
|
||||
Scenario: The reassigned author takes the position of the deleted one
|
||||
Given the article 10 credits the authors 5, 2, 6 in that order
|
||||
When the owner deletes the author 2 with reassign_to=3
|
||||
Then the article 10 credits 5, 3, 6 in that order
|
||||
|
||||
Scenario: A reassignment never duplicates an author on an article
|
||||
Given the article 10 credits the authors 2 then 3
|
||||
When the owner deletes the author 2 with reassign_to=3
|
||||
Then the article 10 credits only the author 3
|
||||
|
||||
Scenario: Nobody removes the byline
|
||||
Given the article 10 credits only the author 2
|
||||
When the owner sends DELETE /v1/author/2?reassign_to=none
|
||||
Then the status is 204
|
||||
And the article 10 has no authors
|
||||
And the article 10 still exists, unchanged otherwise
|
||||
|
||||
Scenario: Only the deleted author is removed from a shared article
|
||||
Given the article 11 credits the authors 5, 2, 6 in that order
|
||||
When the owner sends DELETE /v1/author/2?reassign_to=none
|
||||
Then the article 11 credits 5, 6 in that order
|
||||
|
||||
Scenario: Reassign and delete are all-or-nothing
|
||||
Given the repository fails while deleting the author 2
|
||||
When the owner sends DELETE /v1/author/2?reassign_to=3
|
||||
Then the credits of the author 2 are unchanged
|
||||
And the author 2 still exists
|
||||
|
||||
Scenario: The credits are moved by one repository operation
|
||||
When the owner deletes an author with articles and a valid reassign_to
|
||||
Then the use case calls a single repository operation that moves the credits and deletes the author
|
||||
|
||||
Scenario Outline: An unusable destination
|
||||
Given the author 2 is credited on 3 articles
|
||||
When the owner sends DELETE /v1/author/2?reassign_to=<reassign_to>
|
||||
Then the status is <status>
|
||||
And the body is "<body>"
|
||||
And nothing changed
|
||||
|
||||
Examples:
|
||||
| reassign_to | status | body |
|
||||
| 999 | 404 | author_not_found |
|
||||
| 2 | 400 | invalid_reassign |
|
||||
| abc | 400 | invalid_reassign |
|
||||
| -1 | 400 | invalid_reassign |
|
||||
| (empty) | 400 | invalid_reassign |
|
||||
|
||||
Scenario: A linked author can be deleted and the account is untouched
|
||||
Given the author 2 is linked to the user 3 and credits no article
|
||||
When the owner deletes the author 2
|
||||
Then the user 3 still exists and can create an author again
|
||||
|
||||
Scenario: A missing author is not found
|
||||
When the owner sends DELETE /v1/author/999
|
||||
Then the status is 404
|
||||
And the body is "author_not_found"
|
||||
|
||||
Scenario: An unused reassign_to is ignored
|
||||
Given the author 2 credits no article
|
||||
When the owner sends DELETE /v1/author/2?reassign_to=3
|
||||
Then the status is 204
|
||||
And the author 3 is unchanged
|
||||
|
||||
Scenario Outline: Other roles and keys cannot delete
|
||||
When DELETE /v1/author/2 is sent with <credentials>
|
||||
Then the status is 403
|
||||
And the body is "<body>"
|
||||
And no author changed
|
||||
|
||||
Examples:
|
||||
| credentials | body |
|
||||
| a verified account with another role | Only owners and admins can do this. |
|
||||
| a valid API key | This action needs a real session. |
|
||||
|
||||
Scenario: A role without author rights cannot delete its own author
|
||||
Given the account is linked to the author 4
|
||||
When it sends DELETE /v1/author/4
|
||||
Then the status is 403 with body "Only owners and admins can do this."
|
||||
|
||||
Scenario: An admin can delete
|
||||
When a verified admin sends DELETE /v1/author/2 on an author without articles
|
||||
Then the status is 204
|
||||
|
||||
Rule: Only owner and admin write other people's authors
|
||||
|
||||
Scenario Outline: Refused writes change nothing
|
||||
When <request> is sent with <credentials>
|
||||
Then the status is 403
|
||||
And the body is "<body>"
|
||||
And no author changed
|
||||
|
||||
Examples:
|
||||
| request | credentials | body |
|
||||
| POST /v1/author/ | a verified account with another role | Only owners and admins can do this. |
|
||||
| POST /v1/author/ | a valid API key | This action needs a real session. |
|
||||
| PATCH /v1/author/2 | a verified account with another role | author_rights |
|
||||
|
||||
Rule: Articles credit authors in order
|
||||
|
||||
Scenario: An article is credited with ordered authors
|
||||
Given the authors 5, 6, 7 exist and the article 8 has none
|
||||
When the user sends PATCH /v1/article/8 with {"author_ids": [6, 5, 7]}
|
||||
Then the status is 200
|
||||
And the article body's authors are 6, 5, 7 in that order
|
||||
And the first is the primary byline
|
||||
|
||||
Scenario: The author list is replaced and can be emptied
|
||||
Given the article 8 credits 5, 6
|
||||
When the user sends PATCH /v1/article/8 with {"author_ids": [6]}
|
||||
Then the article 8 credits only 6
|
||||
When the user sends PATCH /v1/article/8 with {"author_ids": []}
|
||||
Then the article 8 has authors []
|
||||
|
||||
Scenario: Reordering changes the positions
|
||||
Given the article 8 credits 5, 6, 7
|
||||
When the user sends PATCH /v1/article/8 with {"author_ids": [7, 5, 6]}
|
||||
Then GET /v1/article/8 lists the authors 7, 5, 6
|
||||
|
||||
Scenario Outline: Any account that can edit the article sets its authors
|
||||
When PATCH /v1/article/8 is sent with {"author_ids": [5]} and <credentials>
|
||||
Then the status is 200
|
||||
|
||||
Examples:
|
||||
| credentials |
|
||||
| the owner's jwt cookie |
|
||||
| a verified admin's jwt cookie |
|
||||
| a verified account with another role |
|
||||
| a valid API key |
|
||||
|
||||
Scenario: The authors and the other fields change together
|
||||
When the user sends PATCH /v1/article/8 with {"body": "New", "site_id": 2, "author_ids": [5]}
|
||||
Then the body, the site_id and the authors are all updated
|
||||
|
||||
Scenario: An update without author_ids keeps the authors
|
||||
Given the article 8 credits 5, 6
|
||||
When the user sends PATCH /v1/article/8 with {"body": "New"}
|
||||
Then the article 8 still credits 5, 6 in that order
|
||||
|
||||
Scenario Outline: An unusable author_ids is refused
|
||||
When the user sends PATCH /v1/article/8 with {"author_ids": <value>}
|
||||
Then the status is <status>
|
||||
And the body is "<body>"
|
||||
And the article is unchanged
|
||||
|
||||
Examples:
|
||||
| value | status | body |
|
||||
| [999] | 404 | author_not_found |
|
||||
| [5, 999] | 404 | author_not_found |
|
||||
| [5, 5] | 400 | apperr.BodyErrorMessage |
|
||||
| null | 400 | apperr.BodyErrorMessage |
|
||||
| "a" | 400 | apperr.BodyErrorMessage |
|
||||
| [-1] | 400 | apperr.BodyErrorMessage |
|
||||
| [0] | 400 | the validator error text |
|
||||
|
||||
Scenario: A refused update changes no authors
|
||||
Given the article 8 credits 5
|
||||
When the user sends PATCH /v1/article/8 with {"body": "New", "author_ids": [999]}
|
||||
Then the status is 404
|
||||
And the article 8 body is unchanged and it still credits 5
|
||||
|
||||
Scenario: An unchanged list records nothing
|
||||
Given the article 8 credits 5, 6
|
||||
When the user sends PATCH /v1/article/8 with {"author_ids": [5, 6]}
|
||||
Then the status is 200
|
||||
And no activity event is recorded
|
||||
|
||||
Scenario: An article without authors can be published
|
||||
Given the article 8 has no authors and is offline
|
||||
When the user sends PATCH /v1/article/8/publish
|
||||
Then the status is 200
|
||||
And the article is online
|
||||
|
||||
Scenario Outline: Article reads carry the ordered authors
|
||||
Given the article 8 credits the authors 6 then 5
|
||||
When <request> is sent
|
||||
Then the article in the response has "authors" listing 6 then 5 with only id, name, slug, bio, avatar_id, website and social_links
|
||||
|
||||
Examples:
|
||||
| request |
|
||||
| GET /v1/article/8 |
|
||||
| GET /v1/article/ (each item of items) |
|
||||
| POST /v1/article |
|
||||
| PATCH /v1/article/8 |
|
||||
| PATCH /v1/article/8/publish |
|
||||
| PATCH /v1/article/8/unpublish |
|
||||
|
||||
Scenario: An article without authors returns an empty list
|
||||
When GET /v1/article/8 is sent on an article with no authors
|
||||
Then "authors" is []
|
||||
|
||||
Scenario: Anonymous readers see the authors of online articles
|
||||
Given the article 9 is online and credits the linked author 5
|
||||
When a caller without credentials sends GET /v1/article/9
|
||||
Then the authors are returned
|
||||
And the body has no user_id, no user_email and no article_count in any author
|
||||
|
||||
Scenario: An offline article is still not found publicly
|
||||
When a caller without a verified account sends GET /v1/article/{offline id}
|
||||
Then the status is 404 with body "Article not found"
|
||||
|
||||
Scenario: A new article defaults to the creator's author
|
||||
Given the caller is linked to the author 5
|
||||
When it sends POST /v1/article with {"title": "Spring release", "body": "Hello"}
|
||||
Then the article is offline
|
||||
And its authors are [5]
|
||||
|
||||
Scenario: A creator without an author creates an article without authors
|
||||
Given the caller has no author
|
||||
When it sends POST /v1/article with {"title": "Spring release", "body": "Hello"}
|
||||
Then its authors are []
|
||||
|
||||
Scenario: Deleting an article removes its credits and keeps the author
|
||||
Given the article 8 credits the author 5, who credits 2 articles
|
||||
When the article 8 is deleted
|
||||
Then the author 5 still exists with article_count 1
|
||||
|
||||
Scenario: article_count counts offline articles too
|
||||
Given the author 5 credits one online and one offline article
|
||||
Then the author 5 has article_count 2
|
||||
|
||||
Scenario: The list filter and counts are unaffected
|
||||
When GET /v1/article/?status=online&q=spring is sent
|
||||
Then the items and counts are as before this slice, with authors on each item
|
||||
|
||||
Rule: The current user carries the linked author
|
||||
|
||||
Scenario: GET /v1/auth/me includes the linked author
|
||||
Given the caller is linked to the author 5
|
||||
When GET /v1/auth/me is sent
|
||||
Then the body has the existing user fields and "author" holding the author 5 (id, name, slug, bio, avatar_id, website, social_links, article_count) with no user_email
|
||||
|
||||
Scenario: A user without an author has author null
|
||||
When GET /v1/auth/me is sent by an account without an author
|
||||
Then "author" is null
|
||||
|
||||
Scenario: The field follows link changes
|
||||
Given the caller was unlinked by an admin
|
||||
When GET /v1/auth/me is sent
|
||||
Then "author" is null
|
||||
|
||||
Scenario: The existing fields are unchanged
|
||||
When GET /v1/auth/me is sent
|
||||
Then the body still has id, username, email, is_verified, created_at, updated_at, display_name and role
|
||||
And no password, token or code is present
|
||||
|
||||
Scenario: An API key reads the same body
|
||||
When GET /v1/auth/me is sent with a valid API key
|
||||
Then "author" is present as for a session
|
||||
|
||||
Rule: Media usage by avatars
|
||||
|
||||
Scenario: An image used as an avatar is counted as used
|
||||
Given the image 5 is the avatar of the author 2 and appears in no article or site
|
||||
When a verified account looks up the usage of the image 5
|
||||
Then used_in counts the author and the image is not "Unused"
|
||||
|
||||
Scenario: The media list counts the author
|
||||
Given the image 5 is used by an article, a site and an author
|
||||
When a verified account sends GET /v1/media/
|
||||
Then the used_in of the image 5 is 3
|
||||
|
||||
Scenario: Deleting the image clears the author's avatar
|
||||
When the owner deletes the image 5
|
||||
Then the status is 204
|
||||
And the author 2 has avatar_id null
|
||||
|
||||
Scenario: Merging copies repoints the author
|
||||
Given the image 6 is a copy of the image 5 and the author 2 uses the image 6
|
||||
When the copies are merged into the image 5
|
||||
Then the author 2 has avatar_id 5
|
||||
And the merge response reports authors_updated 1
|
||||
|
||||
Scenario: Purge repoints authors the same way
|
||||
Given a purge group whose kept image is 5 and whose duplicate 6 is used by the author 2
|
||||
When the purge runs
|
||||
Then the author 2 has avatar_id 5
|
||||
And the purge result reports authors_updated 1
|
||||
|
||||
Scenario: A failed purge group leaves the author pointing at an existing image
|
||||
Given the purge of a group fails midway
|
||||
Then every author avatar_id refers to an image that still exists
|
||||
|
||||
Scenario: The usage lookup array shape is unchanged
|
||||
When a verified account looks up the usage of an image
|
||||
Then the response keeps the same array shape as before this slice
|
||||
|
||||
Rule: Every author write is recorded in the activity log
|
||||
|
||||
Scenario Outline: A successful write is recorded once after the change
|
||||
When <actor> performs <request>
|
||||
Then one event exists with action "<action>", category "authors", target {"type": "author", "id": "<id>", "name": "<name>"}, outcome "success", actor <actor> and source "web"
|
||||
And the event was recorded after the change was applied
|
||||
|
||||
Examples:
|
||||
| actor | request | action | id | name |
|
||||
| the owner | POST /v1/author/ for "Camille Roche" | author.create | 2 | Camille Roche |
|
||||
| the caller | POST /v1/author/me for "Marta Alves" | author.create | 3 | Marta Alves |
|
||||
| the owner | PATCH /v1/author/2 renaming it | author.edit | 2 | Camille R. |
|
||||
| the owner | PATCH /v1/author/2 linking the user "marta" | author.edit | 2 | Camille R. |
|
||||
| the owner | DELETE /v1/author/2 | author.delete | 2 | Camille R. |
|
||||
|
||||
Scenario: The change fields are limited and carry no email
|
||||
When the owner edits an author's name, slug, bio, website, social links, avatar and linked user
|
||||
Then the changes use only the fields name, slug, bio, website, social_links, avatar and user
|
||||
And the user change holds usernames, never an email
|
||||
|
||||
Scenario: An own-author edit by a role without author rights is recorded
|
||||
When the account with another role edits its own author
|
||||
Then one event "author.edit" exists with that account as actor
|
||||
|
||||
Scenario Outline: A refused write is recorded as failed with a plain reason
|
||||
When the actor performs <request> which is refused with <code>
|
||||
Then one event exists with action "<action>", outcome "failed" and failure_reason "<reason>"
|
||||
|
||||
Examples:
|
||||
| request | code | action | reason |
|
||||
| POST /v1/author/ with a taken slug | author_slug_taken | author.create | That slug is already used by another author. |
|
||||
| POST /v1/author/me by a linked caller | user_already_linked | author.create | That user already has an author. |
|
||||
| PATCH /v1/author/2 with a taken slug | author_slug_taken | author.edit | That slug is already used by another author. |
|
||||
| PATCH /v1/author/2 linking a linked user | user_already_linked | author.edit | That user already has an author. |
|
||||
| PATCH /v1/author/2 by a non-admin on it | author_rights | author.edit | Only an owner or admin can change this author. |
|
||||
| DELETE /v1/author/2 with articles | author_has_articles | author.delete | The author is still credited on articles. |
|
||||
| DELETE /v1/author/2 reassigned to itself | invalid_reassign | author.delete | The destination author is not valid. |
|
||||
| POST /v1/author/ when the repository fails with "db is locked" | 500 | author.create | Something went wrong on our side. Please try again. |
|
||||
| DELETE /v1/author/2 when the repository fails | 500 | author.delete | Something went wrong on our side. Please try again. |
|
||||
|
||||
Scenario Outline: Nothing is recorded when there is nothing to attribute
|
||||
When <request> is sent
|
||||
Then no activity event is recorded
|
||||
|
||||
Examples:
|
||||
| request |
|
||||
| PATCH /v1/author/999 as the owner |
|
||||
| DELETE /v1/author/999 as the owner |
|
||||
| POST /v1/author/ with an unknown avatar (image_not_found) |
|
||||
| POST /v1/author/ with a bio over 280 characters |
|
||||
| POST /v1/author/ as a role without author rights (403) |
|
||||
| POST /v1/author/ with no credentials (401) |
|
||||
| PATCH /v1/author/abc as the owner |
|
||||
| POST /v1/author/ with a malformed body |
|
||||
|
||||
Scenario: A failing audit repository never changes the response
|
||||
Given the audit repository fails on Append
|
||||
When the owner sends POST /v1/author/ with valid values
|
||||
Then the response equals that of a healthy repository
|
||||
And the author was created
|
||||
|
||||
Scenario: Reads are never recorded
|
||||
When GET /v1/author/ is sent
|
||||
Then no activity event is recorded
|
||||
|
||||
Scenario: An article's authors change is recorded in article.edit
|
||||
Given the article 8 "Spring release" credits "Inès Duarte" and "Marta Alves"
|
||||
When the user sends PATCH /v1/article/8 with {"author_ids": [Marta, Inès, Zoe]}
|
||||
Then one event "article.edit" exists with changes [{"field": "authors", "from": "Inès Duarte, Marta Alves", "to": "Marta Alves, Inès Duarte, Zoe Lambert"}]
|
||||
|
||||
Scenario: Deleting an author with reassignment records only the author event
|
||||
When the owner deletes the author 2 with reassign_to=3 on 3 articles
|
||||
Then exactly one event "author.delete" exists
|
||||
And no "article.edit" event exists
|
||||
|
||||
Scenario: The activity API lists author events
|
||||
Given author events were recorded
|
||||
When a verified account sends GET /v1/activity/?category=authors
|
||||
Then the author events are returned with category "authors" and no other category
|
||||
|
||||
Rule: Persistence and migration
|
||||
|
||||
Scenario: The new tables are migrated
|
||||
When database.Migrate runs on an empty database
|
||||
Then the authors and article_authors tables exist
|
||||
And the authors table has unique indexes on slug and on user_id, where many NULL user_id values are allowed
|
||||
And article_authors has a unique pair (article_id, author_id) and a position
|
||||
|
||||
Scenario: Existing articles get no authors
|
||||
Given a database from before this slice holding 4 articles
|
||||
When database.Migrate runs
|
||||
Then all 4 articles have no authors and no author is created
|
||||
|
||||
Scenario: It works on every database driver
|
||||
When the repository contract suite runs
|
||||
Then the author repository behaves the same on SQLite, MySQL and PostgreSQL
|
||||
And the SQLite schema test accounts for the new tables
|
||||
|
||||
Scenario: Order is stable
|
||||
Given the article 8 credits 7, 5, 6
|
||||
When it is read back from the repository
|
||||
Then the authors come back as 7, 5, 6
|
||||
|
||||
Rule: Documentation
|
||||
|
||||
Scenario: The docs describe the new surface
|
||||
Then docs/api.md documents the /v1/author routes with auth and roles, the error tokens, the author shape, article author_ids and authors, the author field of /v1/auth/me and the authors_updated media field
|
||||
And docs/architecture.md mentions the author aggregate, its repository and its use cases
|
||||
@@ -0,0 +1,111 @@
|
||||
# QA procedure: Authors backend
|
||||
|
||||
Spec: `features/authors_backend.feature`. Go tests only, in `test/` (package `test`), run with `task test`. No live server, no real SMTP, no real SQLite file outside the temp-dir repository-contract harness. Call `validator.InitValidator()` before any handler call. Every error assertion compares the plain-text body to the exact code token or constant named in the spec.
|
||||
|
||||
## Setup
|
||||
|
||||
- New files `test/author_*_test.go`. Fakes have unique names: `mockAuthorRepo` (authors, slug and user_id uniqueness, article counts, ordered article credits, the reassign-and-delete as one recorded operation, failure and unique-violation injection), `mockAuthorUserRepo` (users with ids, usernames, display names, emails and roles), `mockAuthorImageRepo` (images, avatar usage, merge and purge repointing), and an extended `mockArticleRepo` (ordered author credits, create with default author, delete removes credits). A fake audit repository with fail and panic switches is reused.
|
||||
- Routers mirror `api/router.go`: a new `NewAuthorRouter`, plus the article, auth (`/me`), media and activity routers. Protected routes sit behind `jwtauth.Verifier` + `jwtauth.Authenticator` with a test `jwtauth.New("HS256", ...)`. Requests carry signed `jwt` cookies with the `user_id` claim for: an owner, an admin, a verified account whose role is `"editor"` (a value other than owner or admin, because no third role exists yet), and an unverified account. An API-key request uses the key authentication path.
|
||||
- Helper builders create authors, link users and credit articles directly in the fakes; assertions read the fakes, never the JSON alone.
|
||||
|
||||
## A. Reading
|
||||
|
||||
1. Newest-first list with `total`, `page`, `size` and `article_count`.
|
||||
2. Credentials table: owner, admin, "editor" and API key give 200; unverified gives 403; none gives 401.
|
||||
3. `q` table: name, slug, case-insensitive, no match gives total 0.
|
||||
4. Pagination table: page 2 of 30, size 5, out-of-range page gives empty items, `size=30`, `page=0`, `page=abc` give 400 `apperr.BodyErrorMessage`.
|
||||
5. Email visibility: owner and admin see `user_email`; "editor" gets a body without any `user_email` key or the email string (substring check on the raw body).
|
||||
6. Guest author: `is_linked` false, `user_id` null. Null and empty defaults for avatar, website, bio, links.
|
||||
|
||||
## B. Creating
|
||||
|
||||
7. Owner and admin create: 201, full body, stored, `article_count` 0, `is_linked` false.
|
||||
8. Only the name given: generated slug, defaults.
|
||||
9. Slug generation table (accents folded, punctuation runs, `!!!` gives `author`).
|
||||
10. Generated and explicit taken slug: 409 `author_slug_taken`, nothing stored. Repository unique-violation injection gives the same 409.
|
||||
11. Validation table (name, slug, bio, website, links, link type, avatar) with exact status and token; 422 rows assert the token; boundary table (100-character name and slug, 280 characters and 280 multi-byte characters, 5 links, two custom links, empty website).
|
||||
12. Malformed JSON gives 400 `apperr.BodyErrorMessage`; "editor" with malformed JSON still gets 403 "Only owners and admins can do this.".
|
||||
13. `user_id` on create: success, 404 `user_not_found`, 409 `user_already_linked`.
|
||||
|
||||
## C. Own author (`POST /me`)
|
||||
|
||||
14. Naming table: display name, then username fallback; `user_id` is the caller's; `is_linked` true.
|
||||
15. Owner, admin and "editor" all get 201.
|
||||
16. Slug de-duplication: `marta-alves-2` taken, result `-3`; the 100-character slug case stays within 100 and ends with `-2`.
|
||||
17. Already linked: 409 `user_already_linked`, nothing added; unique-violation injection on `user_id` gives the same 409.
|
||||
18. API key gives 403 "This action needs a real session."; unverified gives the existing 403; none gives 401.
|
||||
|
||||
## D. Editing
|
||||
|
||||
19. Full edit by owner; partial edit keeps absent fields; nullable clear table (avatar, website null and "", links [], bio "").
|
||||
20. Avatar set; social links replaced as a whole.
|
||||
21. Validation table like creation; own slug re-sent is 200; another author's slug is 409; unchanged author after each refusal.
|
||||
22. `{}`, unknown-only and `{"name": null}` give 400 `apperr.BodyErrorMessage`; missing author 404 `author_not_found`; `abc` id gives `apperr.IdUint32ErrorMessage`.
|
||||
23. No-op edit: 200 and no audit event.
|
||||
24. "editor" table: own author 200 (also name, slug, avatar, links); another author 403 `author_rights`; missing id 404; `user_id` in the body of its own author 403 `author_rights` and still linked; API key gives 403 "This action needs a real session."
|
||||
|
||||
## E. Linking and unlinking
|
||||
|
||||
25. Link a guest author: 200, `user_email` visible to the owner.
|
||||
26. Table: linked user gives 409 `user_already_linked`; already-linked author gives 409 `author_already_linked`; same user is a 200 with no event; unknown user gives 404 `user_not_found`.
|
||||
27. Unlink: 200, `is_linked` false, no `user_email`; the user can create an author again; the author keeps its article credits; unlinking a guest is a no-op with no event.
|
||||
28. After unlinking, an article created by that user has no authors.
|
||||
|
||||
## F. Deleting
|
||||
|
||||
29. No articles: 204. With articles and no `reassign_to`: 409 `author_has_articles`, unchanged.
|
||||
30. Reassign: credits move, author gone; position is preserved (5, 2, 6 becomes 5, 3, 6); no duplicate when the target already credits the article (2, 3 becomes 3).
|
||||
31. `none`: only that author is removed from shared articles, order of the rest is kept; articles with only that author end with none and still exist.
|
||||
32. Single repository operation (the fake records one call). Failure injected in it leaves credits and author unchanged.
|
||||
33. `reassign_to` table: 999 gives 404 `author_not_found`; own id, `abc`, `-1`, empty give 400 `invalid_reassign`. Unused valid `reassign_to` on an author without articles gives 204 and leaves the destination untouched. Missing author gives 404.
|
||||
34. Linked author deleted: user still exists and can create again.
|
||||
35. Refusals: "editor" gives 403 "Only owners and admins can do this." including on its own author; API key gives 403 "This action needs a real session."; admin gives 204.
|
||||
|
||||
## G. Articles
|
||||
|
||||
36. `author_ids` PATCH sets, replaces, empties and reorders; reads (GET one, GET list, POST, PATCH, publish, unpublish) carry `authors` in order with only the seven public fields; empty is `[]`.
|
||||
37. Callers table: owner, admin, "editor", API key all give 200.
|
||||
38. Combined PATCH with `body` and `site_id`; omission keeps the authors; a refused `author_ids` (404) leaves body and credits unchanged.
|
||||
39. Unusable table: `[999]`, `[5,999]` 404 `author_not_found`; `[5,5]`, `null`, `"a"`, `[-1]` give 400 `apperr.BodyErrorMessage`; `[0]` gives the validator error text. The article is unchanged in every case.
|
||||
40. Unchanged list is 200 with no event. Publishing an article with no authors succeeds.
|
||||
41. Anonymous read of an online article includes `authors` but no `user_id`, `user_email` or `article_count` (substring check); an offline article is 404 "Article not found".
|
||||
42. Creation default: creator with a linked author gets `[author]`; creator without one gets `[]`; the article is offline.
|
||||
43. Deleting an article removes its credits and keeps the author, `article_count` drops; `article_count` counts offline articles.
|
||||
44. List filters (`status`, `q`, `site`) and `counts` are unchanged apart from `authors` on each item.
|
||||
|
||||
## H. Current user
|
||||
|
||||
45. `/v1/auth/me` returns `author` for a linked caller (seven public fields plus `article_count`, no `user_email`) and null otherwise, null again after an unlink; existing fields are unchanged and no secret appears; API key request returns the same shape.
|
||||
|
||||
## I. Media
|
||||
|
||||
46. Usage lookup counts an avatar, shape unchanged, image not "Unused". Media list `used_in` sums article, site and author usage.
|
||||
47. Deleting the image (204) nulls `avatar_id`.
|
||||
48. Merge repoints authors and reports `authors_updated`; purge does the same; a failed purge group leaves every `avatar_id` pointing at an existing image.
|
||||
|
||||
## J. Activity log
|
||||
|
||||
49. Success table: one event per write with action, category "authors", target type "author", actor, source "web", recorded after the change (including `/me`).
|
||||
50. Change fields are limited to name, slug, bio, website, social_links, avatar, user; the user change shows usernames and no email (substring check on the stored event).
|
||||
51. Failed table with the plain reasons; unknown ids, 403, 401, 422 and other validation refusals and malformed requests record nothing; reads record nothing.
|
||||
52. Failing and panicking audit repository leave the response and the change as with a healthy one.
|
||||
53. `article.edit` with an authors change holds `{"field":"authors","from":"Inès Duarte, Marta Alves","to":"Marta Alves, Inès Duarte, Zoe Lambert"}`; delete with reassignment records one `author.delete` and no `article.edit`.
|
||||
54. `GET /v1/activity/?category=authors` returns only author events for a verified session (an API key gets the route's existing session refusal). Failure reasons use the style of `auditFailures.go`: the author sentinels map to the plain sentences of the feature and any other error gives `GenericFailureReason`.
|
||||
|
||||
## K. Persistence and migration
|
||||
|
||||
55. Repository-contract harness (temp-dir SQLite; MySQL and Postgres opt-in through the existing env switches): author repository contract (CRUD, slug and user uniqueness with many NULL users, search, newest-first pagination, ordered credits and stable read-back, unique (article, author) pair, transactional reassign-and-delete incl. the no-duplicate and position rules). `database.Migrate` creates both tables and leaves existing articles without authors; `test/sqlite_schema_unchanged_test.go` is updated for the new tables.
|
||||
|
||||
## L. Quality gates
|
||||
|
||||
56. Layering guard: use cases import no persistence, mailer, models, gorm, net/http or chi; the domain author package imports no GORM or HTTP; handlers import no persistence; validation uses validate tags and `validator.Validate.Struct`; no comments added.
|
||||
57. Scoped mutation run through `task mutate` only, for example `MUTATE_FILES=<author and article use case files> MUTATE_RUN='TestAuthor' MUTATE_WORKERS=2 task mutate -- <use case package path>`; report LIVED and NOT COVERED mutants; re-check filtered suspects without `MUTATE_RUN`. Also run `task lint:go` and `task fmt`.
|
||||
58. Docs, checked by reading the files: docs/api.md documents the five /v1/author routes (GET, POST, POST /me, PATCH, DELETE) with auth and roles, error tokens, the author and article-author shapes, `author_ids`, the `/auth/me` `author` field and `authors_updated`; docs/architecture.md names the author aggregate, repository and use cases.
|
||||
|
||||
## Manual
|
||||
|
||||
- MySQL and Postgres: run the contract suite with the opt-in env vars against real servers.
|
||||
- Real boot with an old database copy: both new tables appear, articles keep working with no authors, second boot changes nothing.
|
||||
- Docker with the `/data` volume: same boot check.
|
||||
- Read the activity log screen and the CSV export: author events show names, failure reasons and no emails.
|
||||
- Not covered here: the SPA (later slices).
|
||||
Reference in new issue
Block a user