feat(API): media field type
This commit is contained in:
1 parent
ad5faf3d19
commit
a2d596f4ca
63 files changed
+2567
-259
No files matched your search
@@ -39,7 +39,7 @@ Feature: Owners and admins define custom content types, and users manage entries
|
||||
And a field response has exactly the keys key, label, type, required, unique, default, min, max,
|
||||
options, with null for an unset default, min and max and [] for no options
|
||||
And an entry response has exactly the keys id, content_type, data, created_by, updated_by,
|
||||
created_at, updated_at, where content_type is the type's key
|
||||
created_at, updated_at, media, where content_type is the type's key
|
||||
And errors are plain text from http.Error, and the new apperr strings are
|
||||
content_type_not_found, content_type_key_taken, content_type_has_entries,
|
||||
invalid_content_type_key, invalid_field, schema_incompatible, entry_not_found and invalid_entry
|
||||
@@ -201,7 +201,7 @@ Feature: Owners and admins define custom content types, and users manage entries
|
||||
When the client sends POST /v1/content-type/recipe/entry with body
|
||||
{"data": {"title": "Pad thai", "difficulty": "easy"}}
|
||||
Then the response status is 201
|
||||
And the body has exactly the keys id, content_type, data, created_by, updated_by, created_at, updated_at
|
||||
And the body has exactly the keys id, content_type, data, created_by, updated_by, created_at, updated_at, media
|
||||
And content_type is "recipe", created_by and updated_by are 2
|
||||
And data is {"title": "Pad thai", "difficulty": "easy", "servings": 4, "vegetarian": false}
|
||||
And data has no "published_on" and no "notes" key
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
Feature: Media field type for custom content types
|
||||
|
||||
As a site owner or admin
|
||||
I want a content type field that holds one image from the media library
|
||||
So that entries link to the library image instead of keeping a copy
|
||||
|
||||
Background:
|
||||
Given internal/domain/contenttype adds the field type "media" next to text, richtext, number, boolean, date and select
|
||||
And a media field accepts only label, key and required; unique, default, min, max and options are refused with ErrInvalidField
|
||||
And a stored media value is the id of a library image (uint32, at least 1) or null
|
||||
And the entry rules required and type apply to a media field; min, max, options and unique never apply
|
||||
And entry.Validate stays pure; the check that an image exists in the library is done by CreateEntry and UpdateEntry through the domain image.Repository
|
||||
And CreateEntry, UpdateEntry, GetEntry and ListEntries receive image.Repository, wired only in api/dependecyInjection.go
|
||||
And a changed value means a value that is not equal to the id the entry holds in the stored document for that key; on POST every non-null value is changed; on PUT the value is unchanged only when the id equals the stored one
|
||||
And a changed or new non-null value must be an existing library image, else 422 "invalid_entry: <key>: type"
|
||||
And an unchanged stored id whose image was deleted since passes the type check
|
||||
And a required media field is never saved empty: null or absent gives 422 "invalid_entry: <key>: required", and so does an unchanged stored id whose image no longer exists
|
||||
And rules are checked in field order for Validate, then library existence in field order, then unique, and the body names one field per answer
|
||||
And an entry response keeps its seven keys (id, content_type, data, created_by, updated_by, created_at, updated_at) and adds the key "media"
|
||||
And data keeps the image id (or null) as the value of each media field
|
||||
And "media" is a JSON object keyed by media field key, always present ({} when the type has no media field or every media value is null)
|
||||
And "media"[key] is {"id","filename","width","height","size","thumbnail_url"} for an existing image, with width and height null when unknown, or {"id","missing":true} when the image is gone
|
||||
And thumbnail_url is the image's original path, the same value as the url of a media list item
|
||||
And the same "media" object is on POST 201, PUT 200, GET one and every item of the list
|
||||
And usage in the library means distinct articles, authors, sites and entries; one entry counts once however many of its media fields point at the image
|
||||
And GET /v1/media/ and GET /v1/media/{id} items carry used_in = articles + authors + sites + entries, and the counts.unused filter counts entries as uses
|
||||
And GET /v1/media/{id}/usage stays a JSON array and gains items of type "entry"
|
||||
And an entry usage item is {"type":"entry","id":<entry id>,"fields":[<media field keys>],"content_type_key":"event","content_type_name":"Events","first_field":{"key":<key>,"type":<field type>,"value":<raw stored value or null>}}, with no "title" and no "published"
|
||||
And first_field is the first field of the type in schema order and its value is the raw stored JSON value, never formatted or truncated by the server; the client formats it with the same rule as the entry list and shows "Entry N" for null
|
||||
And article, author and site usage items are unchanged and keep their "title"
|
||||
And usage items are ordered articles by id, authors, sites, then entries by content type name then entry id; the array has no cap and the client shows the first few and the total
|
||||
And the media routes keep their access rules and their plain-text errors (404 image_not_found, 400 uint32 format)
|
||||
And DELETE /v1/media/{id} stays allowed for an image used by entries and leaves their data untouched
|
||||
And MergeMedia (including the purge run) repoints entry media values from every merged copy to the kept image and reports it in the existing counts
|
||||
And no repository method is added: entries that reference an image are found with contenttype.Repository.List and entry.Repository.ListPage (Unpaged), filtered in Go, so the code works on SQLite, MySQL/MariaDB and PostgreSQL with no JSON operators
|
||||
And schema compatibility does not consult the library
|
||||
And the key set assertions of the earlier custom content type and entry features and tests are amended from seven keys to eight
|
||||
And types: the library images are 7 "lime-plaster.jpg" 1600x900 240000 bytes, 8 "bathroom-tiles.png" 800x600 51200 bytes; the type "event" has title (text, required) then thumbnail (media, label "Thumbnail", optional)
|
||||
And users: admin id 2 (verified) can write types; any verified user can write entries
|
||||
|
||||
Scenario: Creating a content type with a media field
|
||||
Given a valid jwt cookie for admin 2
|
||||
When the client sends POST /v1/content-type with fields title (text, required) and {"key":"thumbnail","label":"Thumbnail","type":"media","required":false,"unique":false,"default":null,"min":null,"max":null,"options":[]}
|
||||
Then the response status is 201
|
||||
And the thumbnail field comes back with type "media", unique false, default null, min null, max null and options []
|
||||
|
||||
Scenario Outline: A media field refuses options it can't have
|
||||
Given a valid jwt cookie for admin 2
|
||||
When the client sends POST /v1/content-type with a media field that has <option>
|
||||
Then the response status is 422
|
||||
And the response body is "invalid_field"
|
||||
And no content type is stored
|
||||
|
||||
Examples:
|
||||
| option |
|
||||
| unique true |
|
||||
| default 7 |
|
||||
| default "lime" |
|
||||
| min 1 |
|
||||
| max 5 |
|
||||
| options ["a"] |
|
||||
|
||||
Scenario: The type of a saved media field never changes
|
||||
Given the type "event" has an entry and a saved media field "thumbnail"
|
||||
When admin 2 sends PUT /v1/content-type/event turning "thumbnail" into a text field
|
||||
Then the response status is 422 with invalid_field and the schema is unchanged
|
||||
When admin 2 sends PUT turning the text field "title" into a media field
|
||||
Then the response status is 422 with invalid_field and the schema is unchanged
|
||||
|
||||
Scenario Outline: Adding a media field to a type that has entries
|
||||
Given the type "event" has 12 entries
|
||||
When admin 2 sends PUT /v1/content-type/event adding a media field "thumbnail" that is <required>
|
||||
Then the response status is <status>
|
||||
And the response body is <message>
|
||||
And the stored schema is <outcome>
|
||||
|
||||
Examples:
|
||||
| required | status | message | outcome |
|
||||
| required | 409 | schema_incompatible | unchanged |
|
||||
| optional | 200 | | updated |
|
||||
|
||||
Scenario: Making a media field required
|
||||
Given the type "event" has a saved optional media field "thumbnail", one entry with image 7 and one entry with null
|
||||
When admin 2 sends PUT making "thumbnail" required
|
||||
Then the response status is 409 with schema_incompatible
|
||||
When the entry holding null is deleted and the PUT is sent again
|
||||
Then the response status is 200
|
||||
|
||||
Scenario: Making a media field required while a stored image is gone
|
||||
Given the type "event" has a saved optional media field "thumbnail" and one entry holding the id of a deleted image
|
||||
When admin 2 sends PUT making "thumbnail" required
|
||||
Then the response status is 200
|
||||
|
||||
Scenario: Creating an entry with an image
|
||||
Given a valid jwt cookie for admin 2
|
||||
When the client sends POST /v1/content-type/event/entry with {"data":{"title":"Lime plaster workshop","thumbnail":7}}
|
||||
Then the response status is 201
|
||||
And data.thumbnail is 7
|
||||
And media is {"thumbnail":{"id":7,"filename":"lime-plaster.jpg","width":1600,"height":900,"size":240000,"thumbnail_url":"<the image path>"}}
|
||||
|
||||
Scenario Outline: An empty optional media field
|
||||
When the client sends POST /v1/content-type/event/entry with <data>
|
||||
Then the response status is 201
|
||||
And data.thumbnail is <stored>
|
||||
And media is {}
|
||||
|
||||
Examples:
|
||||
| data | stored |
|
||||
| {"data":{"title":"A","thumbnail":null}} | null |
|
||||
| {"data":{"title":"A"}} | absent |
|
||||
|
||||
Scenario Outline: Entry values that break a media field are refused and store nothing
|
||||
Given the type "event" has a media field "thumbnail" that is <required>
|
||||
When the client sends POST /v1/content-type/event/entry with <data>
|
||||
Then the response status is 422
|
||||
And the response body is <message>
|
||||
And no entry exists
|
||||
|
||||
Examples:
|
||||
| required | data | message |
|
||||
| required | {"data":{"title":"A"}} | invalid_entry: thumbnail: required |
|
||||
| required | {"data":{"title":"A","thumbnail":null}} | invalid_entry: thumbnail: required |
|
||||
| optional | {"data":{"title":"A","thumbnail":999}} | invalid_entry: thumbnail: type |
|
||||
| optional | {"data":{"title":"A","thumbnail":"7"}} | invalid_entry: thumbnail: type |
|
||||
| optional | {"data":{"title":"A","thumbnail":0}} | invalid_entry: thumbnail: type |
|
||||
| optional | {"data":{"title":"A","thumbnail":-1}} | invalid_entry: thumbnail: type |
|
||||
| optional | {"data":{"title":"A","thumbnail":7.5}} | invalid_entry: thumbnail: type |
|
||||
| optional | {"data":{"title":"A","thumbnail":4294967296}} | invalid_entry: thumbnail: type |
|
||||
| optional | {"data":{"title":"A","thumbnail":true}} | invalid_entry: thumbnail: type |
|
||||
| optional | {"data":{"title":"A","thumbnail":[7]}} | invalid_entry: thumbnail: type |
|
||||
|
||||
Scenario: Replacing the image of an entry
|
||||
Given the entry 5 of type "event" holds thumbnail 7
|
||||
When a verified user sends PUT .../entry/5 with {"data":{"title":"Lime plaster workshop","thumbnail":8}}
|
||||
Then the response status is 200, data.thumbnail is 8 and media.thumbnail.filename is "bathroom-tiles.png"
|
||||
When the user sends PUT with thumbnail null on an optional field
|
||||
Then data.thumbnail is null and media is {}
|
||||
|
||||
Scenario: An image deleted after the entry was saved
|
||||
Given the entry 5 holds thumbnail 7
|
||||
When DELETE /v1/media/7 answers 204
|
||||
And the client sends GET /v1/content-type/event/entry/5
|
||||
Then data.thumbnail is still 7
|
||||
And media is {"thumbnail":{"id":7,"missing":true}}
|
||||
|
||||
Scenario: An unchanged id whose image is gone can still be saved when the field is optional
|
||||
Given the entry 5 holds thumbnail 7 and the image 7 was deleted
|
||||
When the user sends PUT .../entry/5 with a new title and thumbnail 7
|
||||
Then the response status is 200 and data.thumbnail is 7
|
||||
And media.thumbnail is {"id":7,"missing":true}
|
||||
|
||||
Scenario Outline: A changed value must exist even when the old one is gone
|
||||
Given the entry 5 holds thumbnail 7 and the image 7 was deleted
|
||||
When the user sends PUT .../entry/5 with thumbnail <value>
|
||||
Then the response status is <status> and the body is <message>
|
||||
|
||||
Examples:
|
||||
| value | status | message |
|
||||
| 999 | 422 | invalid_entry: thumbnail: type |
|
||||
| 8 | 200 | |
|
||||
| null | 200 | |
|
||||
|
||||
Scenario Outline: A required media field with a missing image can't be saved as is
|
||||
Given the entry 5 of a type whose "thumbnail" is required holds thumbnail 7 and the image 7 was deleted
|
||||
When the user sends PUT .../entry/5 with thumbnail <value>
|
||||
Then the response status is <status> and the body is <message>
|
||||
And the stored entry is <outcome>
|
||||
|
||||
Examples:
|
||||
| value | status | message | outcome |
|
||||
| 7 | 422 | invalid_entry: thumbnail: required | unchanged |
|
||||
| null | 422 | invalid_entry: thumbnail: required | unchanged |
|
||||
| 8 | 200 | | updated |
|
||||
|
||||
Scenario: A create always treats the value as changed
|
||||
Given the image 7 was deleted
|
||||
When the client sends POST with thumbnail 7
|
||||
Then the response status is 422 with invalid_entry: thumbnail: type
|
||||
|
||||
Scenario: Two broken fields name only one
|
||||
Given a body with a required field left empty and a media field pointing at a missing image
|
||||
When the client sends POST
|
||||
Then the response names one field, with the required and type rules of Validate checked before the library existence
|
||||
|
||||
Scenario: Entry list responses carry the media object per item
|
||||
Given 3 entries: one with image 7, one with image 7 deleted, one with null
|
||||
When the client sends GET /v1/content-type/event/entry
|
||||
Then each item keeps its seven keys and adds "media"
|
||||
And the items' media are one existing object, {"thumbnail":{"id":7,"missing":true}} and {}
|
||||
And items, total, page and size are unchanged
|
||||
|
||||
Scenario: Types without a media field are not enriched
|
||||
Given the type "recipe" has no media field
|
||||
When the client sends GET on a recipe entry
|
||||
Then the body has the seven existing keys and "media" is {}
|
||||
And the image repository is not called
|
||||
|
||||
Scenario: Media list items count entries as uses
|
||||
Given the image 8 is used by 3 entries (one of them through two media fields) and by no article, author or site
|
||||
And the image 7 is used by 2 articles and 5 entries
|
||||
When the client sends GET /v1/media/
|
||||
Then item 8 has used_in 3 and item 7 has used_in 7
|
||||
And GET /v1/media/8 has used_in 3
|
||||
And ?filter=unused does not list 8 or 7, and counts.unused does not count them
|
||||
|
||||
Scenario: Usage of an image lists articles and entries
|
||||
Given the image 7 is used by article 12 "Heat pumps" (cover) and by entries 41, 42 and 43 of "event" and entry 9 of "workshop"
|
||||
When the client sends GET /v1/media/7/usage
|
||||
Then the response status is 200 and the body is a JSON array
|
||||
And the article item is {"type":"article","id":12,"title":"Heat pumps","published":true,"fields":["cover"]}
|
||||
And the entry items follow in the order event 41, 42, 43, then workshop 9 (content type name, then id), each with exactly type, id, fields ["thumbnail"], content_type_key, content_type_name and first_field, and no title and no published
|
||||
And a client counts the array for the total
|
||||
|
||||
Scenario Outline: The raw first field in usage
|
||||
Given an entry whose first field "<key>" is of type <type> and holds <stored>
|
||||
When the client reads the usage of an image the entry uses
|
||||
Then the entry item first_field is {"key":"<key>","type":"<type>","value":<value>}
|
||||
And no string is built by the server and the item has no "title" key
|
||||
|
||||
Examples:
|
||||
| key | type | stored | value |
|
||||
| title | text | "Lime plaster workshop" | "Lime plaster workshop" |
|
||||
| body | richtext | "\n\nFirst line\n\nSecond" | "\n\nFirst line\n\nSecond" |
|
||||
| seats | number | 18 | 18 |
|
||||
| category | select | "Talk" | "Talk" |
|
||||
| online | boolean | false | false |
|
||||
| starts_on | date | "2026-11-21" | "2026-11-21" |
|
||||
| title | text | null | null |
|
||||
| title | text | the key absent | null |
|
||||
| cover | media | 7 | 7 |
|
||||
|
||||
Scenario: Usage of an unused or unknown image
|
||||
When the client sends GET /v1/media/8/usage with no use
|
||||
Then the body is []
|
||||
When the client sends GET /v1/media/999/usage
|
||||
Then the response status is 404 with apperr.ImageNotFoundErrorMessage
|
||||
When the client sends GET /v1/media/abc/usage
|
||||
Then the response status is 400 with apperr.IdUint32ErrorMessage
|
||||
|
||||
Scenario: Deleting an image used by entries is allowed
|
||||
Given the image 7 is used by 5 entries and 2 articles
|
||||
When the client sends DELETE /v1/media/7
|
||||
Then the response status is 204
|
||||
And the 2 articles lose the image reference as today
|
||||
And the 5 entries still hold data.thumbnail 7 and are not modified (updated_at and updated_by unchanged)
|
||||
|
||||
Scenario: Merging duplicates repoints entry media
|
||||
Given the images 7 and 9 share the same sha256 and entries 41 and 42 point at 9
|
||||
When the client sends POST /v1/media/merge with keep_id 7 and merge_ids [9]
|
||||
Then the response status is 200
|
||||
And entries 41 and 42 hold thumbnail 7, with created_by, updated_by and updated_at unchanged
|
||||
And image 9 no longer exists
|
||||
|
||||
Scenario: A failure while repointing entries stops the merge
|
||||
Given the entry repository fails on the second Update
|
||||
When the client sends POST /v1/media/merge
|
||||
Then the response status is 500, the entries already repointed are restored, and no image is removed
|
||||
|
||||
Scenario: The purge plan counts entries as uses
|
||||
Given the duplicate group 7, 9 where only 9 is used (by 1 entry)
|
||||
When the purge preview and the purge run
|
||||
Then 9 is the kept image and 7 is the merged copy
|
||||
|
||||
Scenario: Use case level
|
||||
Given fake contenttype, entry and image repositories
|
||||
Then entry.Validate returns an error satisfying errors.Is(err, entry.ErrInvalidEntry) with Key "thumbnail" and Rule "required" or "type" for the shapes above
|
||||
And CreateEntry and UpdateEntry return the same for a missing library image and perform no Create or Update
|
||||
And UpdateContentType returns contenttype.ErrSchemaIncompatible for a new required media field and does not call Update
|
||||
And contenttype.ValidateFields returns contenttype.ErrInvalidField for a media field with unique, default, min, max or options
|
||||
@@ -0,0 +1,59 @@
|
||||
# QA procedure: media field type (slice A, API only)
|
||||
|
||||
Package: `test` (black-box). No live server, no real SQLite file, no real SMTP, no SPA.
|
||||
|
||||
## Setup shared by every case
|
||||
1. Call `validator.InitValidator()` once per test.
|
||||
2. Reuse the fakes of the custom content type pair (`mockContentTypeRepo`, `mockEntryRepo`) and of the media tests (image, article, site, author, variant file, storage, recorder fakes). Name any new fake uniquely across the package, for example `mockMediaFieldImageRepo`. The image fake must expose `Get`, a `getCalls` counter and a `delete(id)` helper.
|
||||
3. `mockEntryRepo` needs no new method (no repository method is added). Give it an `updateCalls` counter and a `failUpdateOn(n)` switch for the merge compensation case.
|
||||
4. Build `NewHandler(...)` for the entry, content type and media handlers over use cases wired to the fakes (the entry use cases now take the image fake). Mount them on a fresh `chi.NewRouter()` mirroring api/router.go: `/v1/content-type` and `/v1/media`, with `jwtauth.Verifier`, `jwtauth.Authenticator` and a test `jwtauth.New("HS256", []byte("test-secret"), nil)`. Add `RequireOwnerOrAdmin` on the content type write routes. If `api.InitBackendRoutes` with a partial `*api.UseCases` works, prefer it.
|
||||
5. Seed users: admin 2 and a plain verified user. Seed images 7 "lime-plaster.jpg" (1600x900, 240000 bytes) and 8 "bathroom-tiles.png" (800x600, 51200 bytes), with distinct paths. Seed the type "event" (title text required, thumbnail media optional) and a variant with thumbnail required.
|
||||
6. Inject a fake clock so `updated_at` > `created_at`.
|
||||
7. Decode entry and list bodies into `map[string]any`; compare numbers as `float64`.
|
||||
|
||||
## Schema
|
||||
8. POST a media field with the allowed shape -> 201; the field has type "media", unique false, default/min/max null, options [].
|
||||
9. Table of refused options (unique true, default 7, default "lime", min 1, max 5, options ["a"]) -> 422 `invalid_field`, repo empty. Assert separately that `default: null` is accepted.
|
||||
10. Type change in both directions on a type with entries -> 422 `invalid_field`, schema byte-identical. On a type with no entries the existing free type change still applies.
|
||||
11. Add a media field to a type with 12 entries: required -> 409 `schema_incompatible`, schema unchanged; optional -> 200, existing entries still readable and `media` is `{}`. Make an optional media field required: 409 while an entry holds null, 200 once none does. Making it required while an entry holds an id of a deleted image -> 200 (compatibility does not consult the library).
|
||||
|
||||
## Entry writes
|
||||
12. POST with image 7 -> 201; `data.thumbnail` is 7 and `media.thumbnail` has exactly `id`, `filename`, `width`, `height`, `size`, `thumbnail_url`; the key set of the entry is the seven existing keys plus `media`.
|
||||
13. POST with null and with the key absent on an optional field -> 201, `media` is `{}` (assert JSON `{}`, not null).
|
||||
14. Refusal table (required: absent, null; any: 999, "7", 0, -1, 7.5, 4294967296, true, [7]): status 422, exact body `invalid_entry: thumbnail: <rule>`, `createCalls == 0`.
|
||||
15. PUT replace 7 -> 8: 200, filename "bathroom-tiles.png". PUT to null on optional: 200, `media` is `{}`.
|
||||
16. After `delete(7)` on the image fake: GET entry 5 -> `data.thumbnail` 7 and `media.thumbnail` equals `{"id":7,"missing":true}` (no other key). PUT with the same 7 on an optional field -> 200, still 7 and still missing. PUT 999 -> 422 type, PUT 8 or null -> 200.
|
||||
17. Required field, image 7 deleted: PUT 7 -> 422 `required`, PUT null -> 422 `required`, entry unchanged; PUT 8 -> 200.
|
||||
18. POST with a deleted id 7 -> 422 `type` (create is always a changed value).
|
||||
19. Ordering: a body with two bad fields names one only: Validate rules first in field order, then the existence check.
|
||||
|
||||
## Reads
|
||||
20. List: 3 entries (existing image, deleted image, null) -> each item has the seven keys plus `media`; values as in the feature; `total`, `page` and `size` unchanged; `items` is `[]` for an empty type.
|
||||
21. A type without a media field: `media` is `{}` and `getCalls == 0` on the image fake. A list of 100 entries pointing at the same image calls `Get` at most once for that id.
|
||||
|
||||
## Media library counts
|
||||
22. Seed: image 8 used by 3 entries (one through two media fields) and no other use; image 7 used by 2 articles and 5 entries. `GET /v1/media/` -> `used_in` 3 and 7; `GET /v1/media/8` -> 3; `?filter=unused` omits both; `counts.unused` excludes both; an image with no use is still counted unused.
|
||||
23. Unchanged existing cases: article, author and site usage counts and the unused counts still pass with no entries in the fake.
|
||||
|
||||
## Usage endpoint
|
||||
24. Image 7 with article 12 (cover), entries 41, 42, 43 of "event" and entry 9 of "workshop": status 200, a JSON array, article item unchanged (keys type, id, title, published, fields). Entry items have exactly the keys `type`, `id`, `fields`, `content_type_key`, `content_type_name`, `first_field`, and no `title` and no `published`. Order: article, then event 41, 42, 43, then workshop 9.
|
||||
25. Raw first-field table (text, richtext with leading blank lines, number 18, select, boolean false, date, text null, key absent, media 7). Assert `first_field` is exactly `{key, type, value}` with the value byte-equal to the stored JSON (richtext unmodified, number as a JSON number, null as JSON `null`). Assert the entry item JSON has no `title` key.
|
||||
26. Unused image -> `[]`; unknown -> 404 `image_not_found` text; `abc` -> 400 `apperr.IdUint32ErrorMessage`.
|
||||
|
||||
## Delete and merge
|
||||
27. DELETE /v1/media/7 used by 5 entries and 2 articles -> 204; articles cleared as before; the fake entry repo records no Update; the 5 entries are byte-identical.
|
||||
28. Merge keep 7, merge [9] with equal sha256 and two entries on 9 -> 200; the entries hold 7; `created_by`, `updated_by` and `updated_at` are unchanged; image 9 is gone. Entries pointing at other images are untouched.
|
||||
29. Merge with `failUpdateOn(2)` -> 500, the first repointed entry is restored to 9, no image deleted, articles/sites/authors repointing rolled back as today.
|
||||
30. Purge preview and run on a duplicate group where only the copy is used by an entry: the used copy is the kept one; the fingerprint changes accordingly.
|
||||
|
||||
## Use case level (no HTTP)
|
||||
31. `contenttype.ValidateFields` -> `ErrInvalidField` for each refused option. `entry.Validate` rows -> `errors.Is(err, entry.ErrInvalidEntry)` with the right key and rule. `CreateEntry`/`UpdateEntry` with a missing library image -> same error and no write. `UpdateContentType` with a new required media field -> `ErrSchemaIncompatible`, no `Update`.
|
||||
|
||||
## Existing tests to amend
|
||||
32. Amend the existing key-set assertions from seven to eight keys in the earlier custom content type and entry feature, spec and test files (decision: `media` is always present). The coder owns this.
|
||||
|
||||
## Manual verification (not exercised by the Go tests)
|
||||
- Real SQLite, then MySQL/MariaDB and PostgreSQL via the driver tasks: create a type with a media field, upload an image, save an entry, delete the image, reload the entry and confirm the id stays and `media` is `{missing:true}`.
|
||||
- Merge duplicates with entries on a copy against real storage and confirm the entries repoint and the file is removed.
|
||||
- Time `GET /v1/media/` with 1,000 entries across a type with a media field (usage lookup loads the entries of such types).
|
||||
- The SPA (builder, editor control, picker, list cell, library badge and detail panel) is slices B and C and is not checked here.
|
||||
Reference in new issue
Block a user