kempo-media 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +26 -1
- package/.github/workflows/test.yml +32 -6
- package/CHANGELOG.md +4 -0
- package/README.md +4 -0
- package/kempo-config.json +16 -0
- package/package.json +4 -2
- package/public/components/MediaWidget.js +50 -0
- package/scripts/run-tests.js +47 -0
- package/tests/dashboard-widget.node-test.js +85 -0
- package/tests/helpers/db.js +19 -0
- package/tests/helpers/preload.js +10 -0
- package/tests/media-api-contract.node-test.js +4 -1
- package/tests/media-data-layer.node-test.js +7 -0
- package/tests/media-migration.node-test.js +6 -0
|
@@ -26,6 +26,25 @@ jobs:
|
|
|
26
26
|
permissions:
|
|
27
27
|
contents: write
|
|
28
28
|
id-token: write
|
|
29
|
+
# The database-backed suites skip themselves when no database is reachable, which reads as a pass.
|
|
30
|
+
# Publishing used to run without one, so a release could go out while the test workflow was red.
|
|
31
|
+
# This is the same service and schema the test workflow uses, so the two gates agree.
|
|
32
|
+
services:
|
|
33
|
+
postgres:
|
|
34
|
+
image: postgres:16-alpine
|
|
35
|
+
env:
|
|
36
|
+
POSTGRES_USER: kempo
|
|
37
|
+
POSTGRES_PASSWORD: kempo
|
|
38
|
+
POSTGRES_DB: kempo_media_test
|
|
39
|
+
ports:
|
|
40
|
+
- 5432:5432
|
|
41
|
+
options: >-
|
|
42
|
+
--health-cmd pg_isready
|
|
43
|
+
--health-interval 10s
|
|
44
|
+
--health-timeout 5s
|
|
45
|
+
--health-retries 5
|
|
46
|
+
env:
|
|
47
|
+
DATABASE_URL: postgresql://kempo:kempo@localhost:5432/kempo_media_test
|
|
29
48
|
steps:
|
|
30
49
|
- uses: actions/checkout@v4
|
|
31
50
|
with:
|
|
@@ -49,9 +68,15 @@ jobs:
|
|
|
49
68
|
- name: Install the host for the test gate
|
|
50
69
|
run: npm install --no-save kempo@latest kempo-files@latest kempo-thumbs@latest kempo-ui@latest kempo-server@latest
|
|
51
70
|
|
|
52
|
-
- name:
|
|
71
|
+
- name: Apply database schema
|
|
72
|
+
run: npx drizzle-kit push --force
|
|
73
|
+
|
|
74
|
+
- name: Run unit tests
|
|
53
75
|
run: npm test
|
|
54
76
|
|
|
77
|
+
- name: Run integration tests
|
|
78
|
+
run: npm run test:integration
|
|
79
|
+
|
|
55
80
|
- name: Set version type
|
|
56
81
|
id: version
|
|
57
82
|
run: |
|
|
@@ -11,9 +11,30 @@ on:
|
|
|
11
11
|
- master
|
|
12
12
|
|
|
13
13
|
jobs:
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
14
|
+
# Unit tier: an in-memory Postgres (PGlite), so there is no service to start and a database
|
|
15
|
+
# suite cannot skip for want of one.
|
|
16
|
+
unit:
|
|
17
|
+
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: '22.x'
|
|
25
|
+
|
|
26
|
+
- run: npm ci
|
|
27
|
+
|
|
28
|
+
# kempo, kempo-files, kempo-thumbs, kempo-ui and kempo-server are peer dependencies, which npm ci does not install.
|
|
29
|
+
- name: Install the host for the test gate
|
|
30
|
+
run: npm install --no-save kempo@latest kempo-files@latest kempo-thumbs@latest kempo-ui@latest kempo-server@latest
|
|
31
|
+
|
|
32
|
+
- name: Run unit tests
|
|
33
|
+
run: npm test
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Integration tier: the whole suite on a real Postgres. An unreachable database fails it.
|
|
37
|
+
integration:
|
|
17
38
|
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
|
18
39
|
runs-on: ubuntu-latest
|
|
19
40
|
services:
|
|
@@ -50,8 +71,9 @@ jobs:
|
|
|
50
71
|
- name: Apply database schema
|
|
51
72
|
run: npx drizzle-kit push --force
|
|
52
73
|
|
|
53
|
-
- name: Run
|
|
54
|
-
run: npm test
|
|
74
|
+
- name: Run integration tests
|
|
75
|
+
run: npm run test:integration
|
|
76
|
+
|
|
55
77
|
|
|
56
78
|
# Same suite against the sibling checkout of the host. This extension resolves imports out of
|
|
57
79
|
# kempo, so it breaks whenever the host moves something it reaches into — this job is what
|
|
@@ -129,6 +151,10 @@ jobs:
|
|
|
129
151
|
run: npx drizzle-kit push --force
|
|
130
152
|
working-directory: kempo-media
|
|
131
153
|
|
|
132
|
-
- name: Run
|
|
154
|
+
- name: Run unit tests
|
|
133
155
|
run: npm test
|
|
134
156
|
working-directory: kempo-media
|
|
157
|
+
|
|
158
|
+
- name: Run integration tests
|
|
159
|
+
run: npm run test:integration
|
|
160
|
+
working-directory: kempo-media
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Dashboard widget.** A **Media** widget on the kempo admin dashboard showing the asset count and the latest uploads, with a **Upload media** button, declared in `widgets` in `kempo-config.json`. Needs a kempo with the dashboard; ignored on older versions.
|
|
8
12
|
### Changed
|
|
9
13
|
- **Media now sits on kempo-files, with thumbnails from kempo-thumbs.** Before, this extension kept its own copy of everything: uploads in the site's `public/media/uploads/` (static serving, no permissions), and `sharp` thumbnails in `public/media/thumbnails/`, parallel to kempo-files' permission-checked library and kempo-thumbs' ffmpeg thumbnails. Anything consuming images had to handle both stacks. Now an asset is a kempo-files file plus a `kempoMediaAsset` row (kind, dimensions, alt text, display name), the **asset id is the file id** (no mapping table), the bytes live in kempo-files' `files/media/` folder, and a thumbnail is whatever kempo-thumbs made. One storage layer, one set of file permissions, one thumbnail path.
|
|
10
14
|
- `kempo-config.json` declares `"dependencies": ["kempo-files", "kempo-thumbs"]`; kempo will not install this extension until both are enabled. kempo-thumbs is required rather than optional so every consumer can rely on `thumbnail` meaning the same thing. Requires `kempo-files` >= 0.1.5, which adds `uploadFile`, caller-chosen ids and download caching.
|
package/README.md
CHANGED
|
@@ -158,3 +158,7 @@ npm test
|
|
|
158
158
|
- **Only assets uploaded through this library are in it.** Other files in kempo-files do not appear in the media grid unless registered with `createMediaAsset`.
|
|
159
159
|
- **A replaced public file can be cached for up to an hour** by browsers (kempo-files' cache lifetime).
|
|
160
160
|
- **No usage tracking.** Deleting an asset does not check whether a page links to it.
|
|
161
|
+
|
|
162
|
+
## Dashboard widget
|
|
163
|
+
|
|
164
|
+
Adds a **Media** widget to the kempo admin dashboard showing the asset count and the latest uploads, with a **Upload media** button. It is declared in `widgets` in `kempo-config.json` and registered when the extension is installed or updated; people only see it if they hold its permission, and can hide, reorder or resize it from the dashboard's Customize dialog. Needs a kempo with the dashboard (see kempo's `docs/dashboard.md`); on an older kempo the entry is ignored.
|
package/kempo-config.json
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
"public-scope": "kempo-media",
|
|
4
4
|
"schema": "./server/db/schema.js",
|
|
5
5
|
"dependencies": ["kempo-files", "kempo-thumbs"],
|
|
6
|
+
"widgets": [
|
|
7
|
+
{
|
|
8
|
+
"key": "library",
|
|
9
|
+
"title": "Media",
|
|
10
|
+
"description": "How many media assets there are and the latest uploads.",
|
|
11
|
+
"icon": "image",
|
|
12
|
+
"element": "k-media-widget",
|
|
13
|
+
"module": "/kempo-media/components/MediaWidget.js",
|
|
14
|
+
"defaultWidth": 2,
|
|
15
|
+
"defaultHeight": 2,
|
|
16
|
+
"minWidth": 1,
|
|
17
|
+
"minHeight": 2,
|
|
18
|
+
"permission": "media:browse",
|
|
19
|
+
"priority": 73
|
|
20
|
+
}
|
|
21
|
+
],
|
|
6
22
|
"permissions": [
|
|
7
23
|
{ "name": "media:browse", "description": "List and view media library assets" },
|
|
8
24
|
{ "name": "media:upload", "description": "Upload new media assets" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kempo-media",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Media library extension for kempo — upload and manage images, video, audio, 3D models, and archives, stored in kempo-files with thumbnails from kempo-thumbs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Dustin Poissant",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"main": "sdk.js",
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "node scripts/build.js",
|
|
11
|
-
"test": "kempo-test -l minimal",
|
|
11
|
+
"test": "node scripts/run-tests.js unit kempo-test -l minimal",
|
|
12
|
+
"test:integration": "node scripts/run-tests.js integration kempo-test -l minimal",
|
|
12
13
|
"link:local": "npm install ../kempo ../kempo-files ../kempo-thumbs ../kempo-server ../kempo-ui --no-save --no-legacy-peer-deps --force"
|
|
13
14
|
},
|
|
14
15
|
"kempo": {
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"kempo-ui": ">=0.4.43"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
37
|
+
"@electric-sql/pglite": "^0.5.9",
|
|
36
38
|
"drizzle-kit": "^0.31.10",
|
|
37
39
|
"kempo-testing-framework": "^1.4.11",
|
|
38
40
|
"postgres": "^3.4.9"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import DashboardWidget from '/kempo/components/DashboardWidget.js';
|
|
2
|
+
import { html, nothing } from '/kempo-ui/lit-all.min.js';
|
|
3
|
+
import { getMediaAssets } from '/kempo-media/sdk.js';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
The size of the media library and the most recent uploads.
|
|
7
|
+
*/
|
|
8
|
+
export default class MediaWidget extends DashboardWidget {
|
|
9
|
+
static properties = {
|
|
10
|
+
...DashboardWidget.properties,
|
|
11
|
+
total: { state: true },
|
|
12
|
+
recent: { state: true }
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
constructor(){
|
|
16
|
+
super();
|
|
17
|
+
this.total = 0;
|
|
18
|
+
this.recent = [];
|
|
19
|
+
this.cta = { label: 'Upload media', icon: 'upload', href: '/admin/extension/kempo-media/', permission: 'media:upload' };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
Public Methods
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
load = async () => {
|
|
27
|
+
const { assets, total } = await this.unwrap(getMediaAssets({ limit: 9 }));
|
|
28
|
+
this.total = total;
|
|
29
|
+
this.recent = assets;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
Rendering
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
renderContent(){
|
|
37
|
+
if(!this.total) return html`<p class="tc-muted">No media yet.</p>`;
|
|
38
|
+
const rows = this.recent.slice(0, this.height * 3);
|
|
39
|
+
return html`
|
|
40
|
+
<div class="h2 m0">${this.total}</div>
|
|
41
|
+
<div class="small tc-muted mb">${this.total === 1 ? 'Asset' : 'Assets'}</div>
|
|
42
|
+
${rows.length ? html`<div class="small tc-muted mbq">Latest</div>` : nothing}
|
|
43
|
+
${rows.map(asset => html`
|
|
44
|
+
<div class="d-f pyq bb"><span class="flex">${asset.originalName || asset.name || asset.id}</span><span class="small tc-muted">${asset.kind}</span></div>
|
|
45
|
+
`)}
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
customElements.define('k-media-widget', MediaWidget);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
import { pathToFileURL } from 'url';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
The two test tiers, then the command to run in each.
|
|
7
|
+
|
|
8
|
+
unit `KEMPO_DB=pglite`: kempo's database module gives an in-process, in-memory Postgres
|
|
9
|
+
(PGlite) holding the tables in drizzle.config.js, so nothing external is needed and a
|
|
10
|
+
database suite can never skip for want of one.
|
|
11
|
+
integration a real Postgres at DATABASE_URL, for the suites that boot a second server process.
|
|
12
|
+
Here an unreachable database fails the run instead of skipping, so it cannot look
|
|
13
|
+
green without having run.
|
|
14
|
+
|
|
15
|
+
`npm test` is the unit tier. `npm run test:integration` runs the whole suite on a real database. CI
|
|
16
|
+
runs both.
|
|
17
|
+
|
|
18
|
+
Usage: node scripts/run-tests.js <unit|integration> <command> [arguments]
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const [tier, command, ...args] = process.argv.slice(2);
|
|
22
|
+
|
|
23
|
+
if(!['unit', 'integration'].includes(tier) || !command){
|
|
24
|
+
console.error('Usage: node scripts/run-tests.js <unit|integration> <command> [arguments]');
|
|
25
|
+
process.exit(2);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const env = { ...process.env };
|
|
29
|
+
|
|
30
|
+
if(tier === 'unit'){
|
|
31
|
+
env.KEMPO_DB = 'pglite';
|
|
32
|
+
delete env.KEMPO_TEST_TIER;
|
|
33
|
+
// Create the database before any test file can install globals that confuse it. See the preload.
|
|
34
|
+
const preload = pathToFileURL(path.resolve(import.meta.dirname, '..', 'tests', 'helpers', 'preload.js')).href;
|
|
35
|
+
env.NODE_OPTIONS = `${env.NODE_OPTIONS || ''} --import=${preload}`.trim();
|
|
36
|
+
} else {
|
|
37
|
+
delete env.KEMPO_DB;
|
|
38
|
+
env.KEMPO_TEST_TIER = 'integration';
|
|
39
|
+
if(!env.DATABASE_URL){
|
|
40
|
+
console.error('The integration tier needs DATABASE_URL pointing at a throwaway Postgres (named *_test) with the schema applied (npx drizzle-kit push).');
|
|
41
|
+
process.exit(2);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const result = spawnSync(command, args, { stdio: 'inherit', env, shell: true });
|
|
46
|
+
|
|
47
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { readFile } from 'fs/promises';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
This extension adds widgets to the kempo admin dashboard through `widgets` in kempo-config.json. Core
|
|
8
|
+
refuses a definition it could not render (and the extension would fail to install), and a module path
|
|
9
|
+
that points at nothing only shows up as a broken tile, so both are checked here.
|
|
10
|
+
|
|
11
|
+
The rules are checked inline so this works against any installed kempo, including one released before
|
|
12
|
+
the dashboard existed. When the installed kempo does have the dashboard, its own validation also runs.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
const config = JSON.parse(await readFile(path.join(root, 'kempo-config.json'), 'utf8'));
|
|
17
|
+
const pkg = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
|
|
18
|
+
const core = await import(pathToFileURL(path.join(root, 'node_modules', 'kempo', 'server', 'utils', 'dashboard', 'helpers.js')).href).catch(() => null);
|
|
19
|
+
|
|
20
|
+
const scope = `/${pkg.kempo?.['public-scope'] || config['public-scope']}/`;
|
|
21
|
+
const permissions = new Set((config.permissions || []).map(p => p.name));
|
|
22
|
+
|
|
23
|
+
const ELEMENT = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
|
|
24
|
+
const KEY = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
25
|
+
const LOCAL_PATH = /^\/(?![/\\])[^\\\s\u0000-\u001f\u007f]*$/;
|
|
26
|
+
const blocks = (value, max) => Number.isInteger(value) && value >= 1 && value <= max;
|
|
27
|
+
|
|
28
|
+
const problems = widget => {
|
|
29
|
+
const { key, title, element, module, defaultWidth = 2, defaultHeight = 1, minWidth = 1, maxWidth = 4, minHeight = 1, maxHeight = 3, actions = [] } = widget;
|
|
30
|
+
const found = [];
|
|
31
|
+
if(typeof key !== 'string' || !KEY.test(key)) found.push('key must be letters, numbers, ".", "_" or "-"');
|
|
32
|
+
if(typeof title !== 'string' || !title.trim() || title.length > 100) found.push('title is required, at most 100 characters');
|
|
33
|
+
if(typeof element !== 'string' || !ELEMENT.test(element)) found.push('element must be a custom element name such as "my-ext-widget"');
|
|
34
|
+
if(typeof module !== 'string' || !LOCAL_PATH.test(module)) found.push('module must be a path on this site beginning with a single "/"');
|
|
35
|
+
if(!blocks(minWidth, 4) || !blocks(maxWidth, 4) || minWidth > maxWidth) found.push('minWidth and maxWidth must be 1 to 4, min no greater than max');
|
|
36
|
+
if(!blocks(minHeight, 3) || !blocks(maxHeight, 3) || minHeight > maxHeight) found.push('minHeight and maxHeight must be 1 to 3, min no greater than max');
|
|
37
|
+
if(!Number.isInteger(defaultWidth) || defaultWidth < minWidth || defaultWidth > maxWidth) found.push('defaultWidth must be within the width bounds');
|
|
38
|
+
if(!Number.isInteger(defaultHeight) || defaultHeight < minHeight || defaultHeight > maxHeight) found.push('defaultHeight must be within the height bounds');
|
|
39
|
+
if(!Array.isArray(actions) || actions.length > 3 || actions.some(action => !action?.label || !LOCAL_PATH.test(action?.href))) found.push('actions must be at most 3 { label, href } with a site path');
|
|
40
|
+
return found;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default {
|
|
44
|
+
'kempo-config.json declares at least one widget': async ({ pass, fail }) => {
|
|
45
|
+
if(!Array.isArray(config.widgets) || !config.widgets.length) return fail('no widgets declared');
|
|
46
|
+
pass(`${config.widgets.length} widget(s)`);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
'every declared widget is a definition the dashboard can render': async ({ pass, fail }) => {
|
|
50
|
+
for(const widget of config.widgets){
|
|
51
|
+
const found = problems(widget);
|
|
52
|
+
if(found.length) return fail(`${widget.key}: ${found.join('; ')}`);
|
|
53
|
+
if(core){
|
|
54
|
+
const [error] = core.normalizeWidget({ ...widget, owner: pkg.name });
|
|
55
|
+
if(error) return fail(`${widget.key}: kempo core refuses it: ${error.msg}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
pass(core ? 'checked inline and by kempo core' : 'checked inline');
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
'every widget module is served from the public scope and exists': async ({ pass, fail }) => {
|
|
62
|
+
for(const { key, module } of config.widgets){
|
|
63
|
+
if(!module.startsWith(scope)) return fail(`${key}: ${module} is outside ${scope}`);
|
|
64
|
+
if(!existsSync(path.join(root, 'public', module.slice(scope.length)))) return fail(`${key}: ${module} does not exist under public/`);
|
|
65
|
+
}
|
|
66
|
+
pass();
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
'a widget permission is one the extension declares, or from core or a dependency when it declares none': async ({ pass, fail }) => {
|
|
70
|
+
for(const { key, permission } of config.widgets){
|
|
71
|
+
if(permission && permissions.size && !permissions.has(permission) && !permission.startsWith('system:')){
|
|
72
|
+
return fail(`${key}: permission "${permission}" is not declared by this extension`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
pass();
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
'every widget module defines the element it is registered with': async ({ pass, fail }) => {
|
|
79
|
+
for(const { key, module, element } of config.widgets){
|
|
80
|
+
const source = await readFile(path.join(root, 'public', module.slice(scope.length)), 'utf8');
|
|
81
|
+
if(!source.includes(`customElements.define('${element}'`)) return fail(`${key}: ${module} never defines <${element}>`);
|
|
82
|
+
}
|
|
83
|
+
pass();
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Shared by the database-backed suites. See scripts/run-tests.js for the tiers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const unitTier = process.env.KEMPO_DB === 'pglite';
|
|
6
|
+
export const integrationTier = process.env.KEMPO_TEST_TIER === 'integration';
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
The suite for a file that belongs to the integration tier, reported as such in the unit tier. It is
|
|
10
|
+
explicit rather than a silent skip, and `npm run test:integration` is where it runs.
|
|
11
|
+
*/
|
|
12
|
+
export const integrationOnly = name => ({
|
|
13
|
+
[`${name} (integration tier)`]: async ({ pass }) => pass('runs in `npm run test:integration`: it needs a real Postgres or a second server process'),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
Seeds kempo's system groups, permissions and settings in-process. Needs kempo >= 4.5.5.
|
|
18
|
+
*/
|
|
19
|
+
export const seedDatabase = async () => (await import('kempo/server/utils/setup/seedDatabase.js')).default({ log: () => {} });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Loaded with `--import` by scripts/run-tests.js in the unit tier, before any test file.
|
|
3
|
+
|
|
4
|
+
The in-memory database is created on first import of kempo's database module. Creating it here, in a
|
|
5
|
+
clean process, keeps the run from depending on which suite imports it first or on globals a test
|
|
6
|
+
file has installed by then.
|
|
7
|
+
*/
|
|
8
|
+
if(process.env.KEMPO_DB === 'pglite'){
|
|
9
|
+
await import('kempo/server/db/index.js');
|
|
10
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unitTier, integrationOnly } from './helpers/db.js';
|
|
1
2
|
import { spawn, execFileSync } from 'child_process';
|
|
2
3
|
import { writeFile, mkdir, rm, readFile, stat } from 'fs/promises';
|
|
3
4
|
import path from 'path';
|
|
@@ -465,6 +466,8 @@ const buildTests = () => {
|
|
|
465
466
|
};
|
|
466
467
|
};
|
|
467
468
|
|
|
468
|
-
export default
|
|
469
|
+
export default unitTier
|
|
470
|
+
? integrationOnly('media API contract')
|
|
471
|
+
: databaseReachable
|
|
469
472
|
? buildTests()
|
|
470
473
|
: skipped('no reachable database — see the "Local development" section of the README (docker compose up -d, then npx drizzle-kit push)');
|
|
@@ -79,6 +79,13 @@ const purge = async () => {
|
|
|
79
79
|
}
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
+
/*
|
|
83
|
+
kempo-files' media folder also lives on disk, and the disk outlives a database. A fresh in-memory
|
|
84
|
+
database has no row for a folder an earlier run left behind, so the first upload would be refused
|
|
85
|
+
("already exists on disk"). This suite owns that folder, so start from none.
|
|
86
|
+
*/
|
|
87
|
+
await rm(path.join(process.cwd(), 'files', 'media'), { recursive: true, force: true }).catch(() => {});
|
|
88
|
+
|
|
82
89
|
const buildTests = () => ({
|
|
83
90
|
'createMediaAsset records every column and stamps timestamps': async ({ pass, fail }) => {
|
|
84
91
|
const [error, row] = await create({ altText: 'A test image' });
|
|
@@ -78,6 +78,12 @@ const cleanup = async () => {
|
|
|
78
78
|
for(const id of Object.values(IDS)) await deleteFile({ id }).catch(() => {});
|
|
79
79
|
await db.delete(kempoMediaAsset).where(inArray(kempoMediaAsset.id, Object.values(IDS))).catch(() => {});
|
|
80
80
|
await db.delete(settingTable).where(eq(settingTable.name, 'kempo-media:thumbnail_max_dimension')).catch(() => {});
|
|
81
|
+
/*
|
|
82
|
+
kempo-files' folders also live on disk, and the disk outlives a database. A run on a fresh in-memory
|
|
83
|
+
database has no row for the media folder an earlier run left behind, and the migration then refuses
|
|
84
|
+
to create it ("already exists on disk"). This suite owns that folder, so reset it.
|
|
85
|
+
*/
|
|
86
|
+
await rm(path.join(process.cwd(), 'files', 'media'), { recursive: true, force: true }).catch(() => {});
|
|
81
87
|
await removeExtensions();
|
|
82
88
|
await db.execute(sql.raw(`ALTER TABLE "kempoMediaAsset" DROP COLUMN IF EXISTS "filename", DROP COLUMN IF EXISTS "path", DROP COLUMN IF EXISTS "thumbnailPath"`)).catch(() => {});
|
|
83
89
|
if(state.site) await rm(state.site, { recursive: true, force: true }).catch(() => {});
|