chadstart 1.0.0 → 1.0.1
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/.devcontainer/devcontainer.json +34 -0
- package/.env.example +10 -1
- package/.github/workflows/db-integration.yml +139 -0
- package/.github/workflows/npm-chadstart.yml +12 -1
- package/.github/workflows/npm-sdk.yml +12 -1
- package/core/api-generator.js +36 -36
- package/core/auth.js +76 -65
- package/core/db.js +324 -149
- package/core/seeder.js +3 -3
- package/docs/config.md +8 -8
- package/package.json +4 -1
- package/server/express-server.js +18 -18
- package/test/access-policies.test.js +8 -8
- package/test/api-keys.test.js +28 -28
- package/test/auth.test.js +18 -18
- package/test/db.test.js +71 -71
- package/test/groups.test.js +5 -5
- package/test/integration/db-integration.test.js +368 -0
- package/test/middleware.test.js +1 -1
- package/test/sdk.test.js +19 -19
- package/test/seeder.test.js +26 -26
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Engine-agnostic HTTP-level integration tests.
|
|
5
|
+
*
|
|
6
|
+
* Runs against a live database specified by DB_ENGINE (postgres | mysql).
|
|
7
|
+
* Execute via: npm run test:integration
|
|
8
|
+
*
|
|
9
|
+
* Required env vars when DB_ENGINE != sqlite:
|
|
10
|
+
* DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, DB_DATABASE
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const assert = require('assert');
|
|
14
|
+
const http = require('http');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
|
|
19
|
+
const { buildApp } = require('../../server/express-server');
|
|
20
|
+
const dbModule = require('../../core/db');
|
|
21
|
+
|
|
22
|
+
const YAML_PATH = path.resolve(__dirname, '../../chadstart.yaml');
|
|
23
|
+
const DB_ENGINE = (process.env.DB_ENGINE || 'sqlite').toLowerCase();
|
|
24
|
+
|
|
25
|
+
// ── HTTP helper ──────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function req(options) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const { method = 'GET', path: p, body, headers = {}, port } = options;
|
|
30
|
+
const data = body ? JSON.stringify(body) : undefined;
|
|
31
|
+
const opts = {
|
|
32
|
+
hostname: 'localhost',
|
|
33
|
+
port,
|
|
34
|
+
path: p,
|
|
35
|
+
method,
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
...headers,
|
|
39
|
+
...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const r = http.request(opts, (res) => {
|
|
43
|
+
let buf = '';
|
|
44
|
+
res.on('data', (c) => { buf += c; });
|
|
45
|
+
res.on('end', () => {
|
|
46
|
+
let json;
|
|
47
|
+
try { json = JSON.parse(buf); } catch { json = buf; }
|
|
48
|
+
resolve({ status: res.statusCode, body: json });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
r.on('error', reject);
|
|
52
|
+
if (data) r.write(data);
|
|
53
|
+
r.end();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Test suite ───────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
describe(`DB integration – ${DB_ENGINE}`, function () {
|
|
60
|
+
this.timeout(30000);
|
|
61
|
+
|
|
62
|
+
let server, port, adminToken, adminId;
|
|
63
|
+
let _sqliteTmpPath; // used only in SQLite mode
|
|
64
|
+
|
|
65
|
+
before(async function () {
|
|
66
|
+
// In SQLite mode (local dev smoke-test), write to a temp file
|
|
67
|
+
if (DB_ENGINE === 'sqlite') {
|
|
68
|
+
_sqliteTmpPath = path.join(os.tmpdir(), `integ-test-${Date.now()}.db`);
|
|
69
|
+
process.env.DB_PATH = _sqliteTmpPath;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { app } = await buildApp(YAML_PATH, null);
|
|
73
|
+
server = http.createServer(app);
|
|
74
|
+
await new Promise((resolve) => server.listen(0, resolve));
|
|
75
|
+
port = server.address().port;
|
|
76
|
+
|
|
77
|
+
// Unique e-mail per run so re-runs on the same DB don't collide
|
|
78
|
+
const email = `integ-admin-${Date.now()}@test.com`;
|
|
79
|
+
const su = await req({
|
|
80
|
+
port, method: 'POST', path: '/api/auth/admin/signup',
|
|
81
|
+
body: { email, password: 'secret123', name: 'Integration Admin' },
|
|
82
|
+
});
|
|
83
|
+
assert.strictEqual(su.status, 201, `Admin signup failed: ${JSON.stringify(su.body)}`);
|
|
84
|
+
adminToken = su.body.token;
|
|
85
|
+
adminId = su.body.user.id;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
after(async function () {
|
|
89
|
+
if (server) await new Promise((resolve) => server.close(resolve));
|
|
90
|
+
await dbModule.closeDb();
|
|
91
|
+
if (_sqliteTmpPath) {
|
|
92
|
+
try { fs.unlinkSync(_sqliteTmpPath); } catch { /* ignore */ }
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── Health ─────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
describe('GET /health', () => {
|
|
99
|
+
it('returns ok', async () => {
|
|
100
|
+
const r = await req({ port, path: '/health' });
|
|
101
|
+
assert.strictEqual(r.status, 200);
|
|
102
|
+
assert.strictEqual(r.body.status, 'ok');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ── Auth ───────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
describe('Auth – Admin collection', () => {
|
|
109
|
+
it('signup returns 201 with token and user', async () => {
|
|
110
|
+
const email = `integ-signup-${Date.now()}@test.com`;
|
|
111
|
+
const r = await req({
|
|
112
|
+
port, method: 'POST', path: '/api/auth/admin/signup',
|
|
113
|
+
body: { email, password: 'pass123', name: 'Signup User' },
|
|
114
|
+
});
|
|
115
|
+
assert.strictEqual(r.status, 201);
|
|
116
|
+
assert.ok(r.body.token, 'should return a token');
|
|
117
|
+
assert.ok(r.body.user.id, 'should return a user id');
|
|
118
|
+
assert.strictEqual(r.body.user.email, email);
|
|
119
|
+
assert.ok(!r.body.user.password, 'password must not be exposed');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('signup returns 409 for duplicate email', async () => {
|
|
123
|
+
const email = `integ-dup-${Date.now()}@test.com`;
|
|
124
|
+
await req({ port, method: 'POST', path: '/api/auth/admin/signup', body: { email, password: 'x' } });
|
|
125
|
+
const r = await req({ port, method: 'POST', path: '/api/auth/admin/signup', body: { email, password: 'y' } });
|
|
126
|
+
assert.strictEqual(r.status, 409);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('login returns 200 with token for valid credentials', async () => {
|
|
130
|
+
const email = `integ-login-${Date.now()}@test.com`;
|
|
131
|
+
await req({ port, method: 'POST', path: '/api/auth/admin/signup', body: { email, password: 'myPass' } });
|
|
132
|
+
const r = await req({ port, method: 'POST', path: '/api/auth/admin/login', body: { email, password: 'myPass' } });
|
|
133
|
+
assert.strictEqual(r.status, 200);
|
|
134
|
+
assert.ok(r.body.token);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('login returns 401 for wrong password', async () => {
|
|
138
|
+
const email = `integ-badpw-${Date.now()}@test.com`;
|
|
139
|
+
await req({ port, method: 'POST', path: '/api/auth/admin/signup', body: { email, password: 'correct' } });
|
|
140
|
+
const r = await req({ port, method: 'POST', path: '/api/auth/admin/login', body: { email, password: 'wrong' } });
|
|
141
|
+
assert.strictEqual(r.status, 401);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('GET /me returns current user', async () => {
|
|
145
|
+
const r = await req({
|
|
146
|
+
port, path: '/api/auth/admin/me',
|
|
147
|
+
headers: { Authorization: `Bearer ${adminToken}` },
|
|
148
|
+
});
|
|
149
|
+
assert.strictEqual(r.status, 200);
|
|
150
|
+
assert.strictEqual(r.body.id, adminId);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('GET /me returns 401 without token', async () => {
|
|
154
|
+
const r = await req({ port, path: '/api/auth/admin/me' });
|
|
155
|
+
assert.strictEqual(r.status, 401);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ── CRUD – Post collection ─────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
describe('CRUD – Post collection', () => {
|
|
162
|
+
let postId;
|
|
163
|
+
|
|
164
|
+
it('POST creates a record (admin-restricted)', async () => {
|
|
165
|
+
const r = await req({
|
|
166
|
+
port, method: 'POST', path: '/api/collections/post',
|
|
167
|
+
body: { title: 'Integration Test Post', content: 'Some content', published: true },
|
|
168
|
+
headers: { Authorization: `Bearer ${adminToken}` },
|
|
169
|
+
});
|
|
170
|
+
assert.strictEqual(r.status, 201, JSON.stringify(r.body));
|
|
171
|
+
assert.ok(r.body.id);
|
|
172
|
+
assert.strictEqual(r.body.title, 'Integration Test Post');
|
|
173
|
+
assert.ok(r.body.createdAt);
|
|
174
|
+
assert.ok(r.body.updatedAt);
|
|
175
|
+
postId = r.body.id;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('POST returns 401 without token', async () => {
|
|
179
|
+
const r = await req({
|
|
180
|
+
port, method: 'POST', path: '/api/collections/post',
|
|
181
|
+
body: { title: 'No Auth Post', content: 'x' },
|
|
182
|
+
});
|
|
183
|
+
assert.strictEqual(r.status, 401);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('GET list returns paginated data (public)', async () => {
|
|
187
|
+
const r = await req({ port, path: '/api/collections/post' });
|
|
188
|
+
assert.strictEqual(r.status, 200);
|
|
189
|
+
assert.ok(Array.isArray(r.body.data));
|
|
190
|
+
assert.ok(typeof r.body.total === 'number');
|
|
191
|
+
assert.ok(typeof r.body.currentPage === 'number');
|
|
192
|
+
assert.ok(r.body.total >= 1, 'should have at least the post we created');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('GET list respects pagination params', async () => {
|
|
196
|
+
const r = await req({ port, path: '/api/collections/post?page=1&perPage=1' });
|
|
197
|
+
assert.strictEqual(r.status, 200);
|
|
198
|
+
assert.strictEqual(r.body.perPage, 1);
|
|
199
|
+
assert.ok(r.body.data.length <= 1);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('GET by id returns the record (public)', async () => {
|
|
203
|
+
const r = await req({ port, path: `/api/collections/post/${postId}` });
|
|
204
|
+
assert.strictEqual(r.status, 200);
|
|
205
|
+
assert.strictEqual(r.body.id, postId);
|
|
206
|
+
assert.strictEqual(r.body.title, 'Integration Test Post');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('GET by id returns 404 for unknown id', async () => {
|
|
210
|
+
const r = await req({ port, path: '/api/collections/post/nonexistent-id' });
|
|
211
|
+
assert.strictEqual(r.status, 404);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('PATCH updates a field (admin-restricted)', async () => {
|
|
215
|
+
const r = await req({
|
|
216
|
+
port, method: 'PATCH', path: `/api/collections/post/${postId}`,
|
|
217
|
+
body: { title: 'Updated Title' },
|
|
218
|
+
headers: { Authorization: `Bearer ${adminToken}` },
|
|
219
|
+
});
|
|
220
|
+
assert.strictEqual(r.status, 200, JSON.stringify(r.body));
|
|
221
|
+
assert.strictEqual(r.body.title, 'Updated Title');
|
|
222
|
+
assert.strictEqual(r.body.content, 'Some content', 'other fields should not change');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('PATCH returns 401 without token', async () => {
|
|
226
|
+
const r = await req({
|
|
227
|
+
port, method: 'PATCH', path: `/api/collections/post/${postId}`,
|
|
228
|
+
body: { title: 'Unauthorized' },
|
|
229
|
+
});
|
|
230
|
+
assert.strictEqual(r.status, 401);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('DELETE removes the record (admin-restricted)', async () => {
|
|
234
|
+
const r = await req({
|
|
235
|
+
port, method: 'DELETE', path: `/api/collections/post/${postId}`,
|
|
236
|
+
headers: { Authorization: `Bearer ${adminToken}` },
|
|
237
|
+
});
|
|
238
|
+
assert.strictEqual(r.status, 200);
|
|
239
|
+
assert.strictEqual(r.body.id, postId);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('GET by id returns 404 after deletion', async () => {
|
|
243
|
+
const r = await req({ port, path: `/api/collections/post/${postId}` });
|
|
244
|
+
assert.strictEqual(r.status, 404);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ── Relations – Comment belongsTo Post ────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
describe('Relations – Comment belongsTo Post', () => {
|
|
251
|
+
let parentPostId, commentId;
|
|
252
|
+
|
|
253
|
+
before(async () => {
|
|
254
|
+
// Create a parent post
|
|
255
|
+
const r = await req({
|
|
256
|
+
port, method: 'POST', path: '/api/collections/post',
|
|
257
|
+
body: { title: 'Parent Post', content: 'For comments', published: true },
|
|
258
|
+
headers: { Authorization: `Bearer ${adminToken}` },
|
|
259
|
+
});
|
|
260
|
+
parentPostId = r.body.id;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('POST comment with post FK', async () => {
|
|
264
|
+
const r = await req({
|
|
265
|
+
port, method: 'POST', path: '/api/collections/comment',
|
|
266
|
+
body: { text: 'Great post!', post_id: parentPostId },
|
|
267
|
+
headers: { Authorization: `Bearer ${adminToken}` },
|
|
268
|
+
});
|
|
269
|
+
assert.strictEqual(r.status, 201, JSON.stringify(r.body));
|
|
270
|
+
assert.ok(r.body.id);
|
|
271
|
+
assert.strictEqual(r.body.post_id, parentPostId);
|
|
272
|
+
commentId = r.body.id;
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('GET comment with ?relations=Post resolves parent', async () => {
|
|
276
|
+
const r = await req({ port, path: `/api/collections/comment/${commentId}?relations=Post` });
|
|
277
|
+
assert.strictEqual(r.status, 200);
|
|
278
|
+
assert.ok(r.body.Post, 'Post relation should be loaded');
|
|
279
|
+
assert.strictEqual(r.body.Post.id, parentPostId);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('GET post list with ?relations=comment resolves children', async () => {
|
|
283
|
+
const r = await req({ port, path: `/api/collections/post/${parentPostId}?relations=comment` });
|
|
284
|
+
assert.strictEqual(r.status, 200);
|
|
285
|
+
assert.ok(Array.isArray(r.body.comment), 'comment relation should be an array');
|
|
286
|
+
assert.ok(r.body.comment.length >= 1);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// ── API Keys ───────────────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
describe('API Keys', () => {
|
|
293
|
+
let apiKey, apiKeyId;
|
|
294
|
+
|
|
295
|
+
it('creates an API key', async () => {
|
|
296
|
+
const r = await req({
|
|
297
|
+
port, method: 'POST', path: '/api/auth/admin/api-keys',
|
|
298
|
+
body: { name: 'IntegKey', permissions: ['read'], entities: [] },
|
|
299
|
+
headers: { Authorization: `Bearer ${adminToken}` },
|
|
300
|
+
});
|
|
301
|
+
assert.strictEqual(r.status, 201);
|
|
302
|
+
assert.ok(r.body.key.startsWith('cs_'));
|
|
303
|
+
assert.strictEqual(r.body.record.name, 'IntegKey');
|
|
304
|
+
assert.ok(!r.body.record.keyHash, 'keyHash must not be exposed');
|
|
305
|
+
apiKey = r.body.key;
|
|
306
|
+
apiKeyId = r.body.record.id;
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('API key authenticates a public route', async () => {
|
|
310
|
+
const r = await req({
|
|
311
|
+
port, path: '/api/collections/post',
|
|
312
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
313
|
+
});
|
|
314
|
+
assert.strictEqual(r.status, 200);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('API key is rejected for a write operation when permission is read-only', async () => {
|
|
318
|
+
const r = await req({
|
|
319
|
+
port, method: 'POST', path: '/api/collections/post',
|
|
320
|
+
body: { title: 'Should fail', content: 'x' },
|
|
321
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
322
|
+
});
|
|
323
|
+
// read-only key: create is not allowed
|
|
324
|
+
assert.strictEqual(r.status, 403);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('lists own API keys', async () => {
|
|
328
|
+
const r = await req({
|
|
329
|
+
port, path: '/api/auth/admin/api-keys',
|
|
330
|
+
headers: { Authorization: `Bearer ${adminToken}` },
|
|
331
|
+
});
|
|
332
|
+
assert.strictEqual(r.status, 200);
|
|
333
|
+
assert.ok(Array.isArray(r.body));
|
|
334
|
+
assert.ok(r.body.some((k) => k.id === apiKeyId));
|
|
335
|
+
assert.ok(r.body.every((k) => !k.keyHash), 'keyHash must never be exposed');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('deletes the API key', async () => {
|
|
339
|
+
const r = await req({
|
|
340
|
+
port, method: 'DELETE', path: `/api/auth/admin/api-keys/${apiKeyId}`,
|
|
341
|
+
headers: { Authorization: `Bearer ${adminToken}` },
|
|
342
|
+
});
|
|
343
|
+
assert.strictEqual(r.status, 200);
|
|
344
|
+
assert.ok(r.body.success);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('deleted API key is rejected on restricted route', async () => {
|
|
348
|
+
// Use /api/auth/admin/me which requires valid auth
|
|
349
|
+
const r = await req({
|
|
350
|
+
port, path: '/api/auth/admin/me',
|
|
351
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
352
|
+
});
|
|
353
|
+
assert.strictEqual(r.status, 401);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ── Admin schema ───────────────────────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
describe('GET /admin/schema', () => {
|
|
360
|
+
it('returns entities and userCollections', async () => {
|
|
361
|
+
const r = await req({ port, path: '/admin/schema' });
|
|
362
|
+
assert.strictEqual(r.status, 200);
|
|
363
|
+
assert.ok(Array.isArray(r.body.entities));
|
|
364
|
+
assert.ok(Array.isArray(r.body.userCollections));
|
|
365
|
+
assert.ok(r.body.userCollections.every((e) => e.authenticable));
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
});
|
package/test/middleware.test.js
CHANGED
|
@@ -25,7 +25,7 @@ describe('runMiddlewares – SDK injection', () => {
|
|
|
25
25
|
|
|
26
26
|
before(async () => {
|
|
27
27
|
tmp = path.join(os.tmpdir(), `chadstart-mw-${Date.now()}.db`);
|
|
28
|
-
dbModule.initDb(mwCore, tmp);
|
|
28
|
+
await dbModule.initDb(mwCore, tmp);
|
|
29
29
|
|
|
30
30
|
functionsDir = path.join(os.tmpdir(), `chadstart-functions-${Date.now()}`);
|
|
31
31
|
fs.mkdirSync(functionsDir, { recursive: true });
|
package/test/sdk.test.js
CHANGED
|
@@ -18,10 +18,10 @@ describe('createBackendSdk', () => {
|
|
|
18
18
|
},
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
before(() => {
|
|
21
|
+
before(async () => {
|
|
22
22
|
tmp = path.join(os.tmpdir(), `chadstart-sdk-${Date.now()}.db`);
|
|
23
|
-
dbModule.initDb(sdkCore, tmp);
|
|
24
|
-
dbModule.create('config', { value: 'initial' });
|
|
23
|
+
await dbModule.initDb(sdkCore, tmp);
|
|
24
|
+
await dbModule.create('config', { value: 'initial' });
|
|
25
25
|
sdk = createBackendSdk(sdkCore);
|
|
26
26
|
});
|
|
27
27
|
|
|
@@ -37,30 +37,30 @@ describe('createBackendSdk', () => {
|
|
|
37
37
|
assert.strictEqual(typeof iface.delete, 'function');
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
it('from().create and findOneById work', () => {
|
|
41
|
-
const book = sdk.from('book').create({ title: 'Dune', author: 'Herbert' });
|
|
40
|
+
it('from().create and findOneById work', async () => {
|
|
41
|
+
const book = await sdk.from('book').create({ title: 'Dune', author: 'Herbert' });
|
|
42
42
|
assert.strictEqual(book.title, 'Dune');
|
|
43
|
-
const found = sdk.from('book').findOneById(book.id);
|
|
43
|
+
const found = await sdk.from('book').findOneById(book.id);
|
|
44
44
|
assert.strictEqual(found.author, 'Herbert');
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
it('from().find returns paginated result', () => {
|
|
48
|
-
const result = sdk.from('book').find();
|
|
47
|
+
it('from().find returns paginated result', async () => {
|
|
48
|
+
const result = await sdk.from('book').find();
|
|
49
49
|
assert.ok(Array.isArray(result.data));
|
|
50
50
|
assert.ok(typeof result.total === 'number');
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
it('from().patch updates a field', () => {
|
|
54
|
-
const book = sdk.from('book').create({ title: 'Old Title', author: 'Author' });
|
|
55
|
-
const updated = sdk.from('book').patch(book.id, { title: 'New Title' });
|
|
53
|
+
it('from().patch updates a field', async () => {
|
|
54
|
+
const book = await sdk.from('book').create({ title: 'Old Title', author: 'Author' });
|
|
55
|
+
const updated = await sdk.from('book').patch(book.id, { title: 'New Title' });
|
|
56
56
|
assert.strictEqual(updated.title, 'New Title');
|
|
57
57
|
assert.strictEqual(updated.author, 'Author');
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
it('from().delete removes a record', () => {
|
|
61
|
-
const book = sdk.from('book').create({ title: 'To Delete', author: 'X' });
|
|
62
|
-
sdk.from('book').delete(book.id);
|
|
63
|
-
assert.strictEqual(sdk.from('book').findOneById(book.id), null);
|
|
60
|
+
it('from().delete removes a record', async () => {
|
|
61
|
+
const book = await sdk.from('book').create({ title: 'To Delete', author: 'X' });
|
|
62
|
+
await sdk.from('book').delete(book.id);
|
|
63
|
+
assert.strictEqual(await sdk.from('book').findOneById(book.id), null);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
it('from() throws for unknown slug', () => {
|
|
@@ -74,13 +74,13 @@ describe('createBackendSdk', () => {
|
|
|
74
74
|
assert.strictEqual(typeof iface.patch, 'function');
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
it('single().get retrieves the record', () => {
|
|
78
|
-
const record = sdk.single('config').get();
|
|
77
|
+
it('single().get retrieves the record', async () => {
|
|
78
|
+
const record = await sdk.single('config').get();
|
|
79
79
|
assert.strictEqual(record.value, 'initial');
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
it('single().patch updates a field', () => {
|
|
83
|
-
const updated = sdk.single('config').patch({ value: 'changed' });
|
|
82
|
+
it('single().patch updates a field', async () => {
|
|
83
|
+
const updated = await sdk.single('config').patch({ value: 'changed' });
|
|
84
84
|
assert.strictEqual(updated.value, 'changed');
|
|
85
85
|
});
|
|
86
86
|
|
package/test/seeder.test.js
CHANGED
|
@@ -35,7 +35,7 @@ describe('seeder', () => {
|
|
|
35
35
|
|
|
36
36
|
before(async () => {
|
|
37
37
|
seedDbPath = path.join(os.tmpdir(), `chadstart-seed-${Date.now()}.db`);
|
|
38
|
-
dbModule.initDb(seedCore, seedDbPath);
|
|
38
|
+
await dbModule.initDb(seedCore, seedDbPath);
|
|
39
39
|
firstSeedResult = await seedAll(seedCore);
|
|
40
40
|
});
|
|
41
41
|
|
|
@@ -46,23 +46,23 @@ describe('seeder', () => {
|
|
|
46
46
|
assert.strictEqual(firstSeedResult.summary.Article, 5);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
it('seedAll inserts rows into the database', () => {
|
|
50
|
-
const authors = dbModule.findAll('author', {}, { perPage: 100 });
|
|
49
|
+
it('seedAll inserts rows into the database', async () => {
|
|
50
|
+
const authors = await dbModule.findAll('author', {}, { perPage: 100 });
|
|
51
51
|
assert.ok(authors.total >= 3);
|
|
52
|
-
const articles = dbModule.findAll('article', {}, { perPage: 100 });
|
|
52
|
+
const articles = await dbModule.findAll('article', {}, { perPage: 100 });
|
|
53
53
|
assert.ok(articles.total >= 5);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
it('seedAll creates authenticable records with email field', () => {
|
|
57
|
-
const authors = dbModule.findAll('author', {}, { perPage: 100 });
|
|
56
|
+
it('seedAll creates authenticable records with email field', async () => {
|
|
57
|
+
const authors = await dbModule.findAll('author', {}, { perPage: 100 });
|
|
58
58
|
for (const a of authors.data) {
|
|
59
59
|
assert.ok(typeof a.email === 'string' && a.email.includes('@'));
|
|
60
60
|
assert.ok(typeof a.password === 'string' && a.password.length > 0);
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
it('seedAll links belongsTo FK to a seeded parent', () => {
|
|
65
|
-
const articles = dbModule.findAll('article', {}, { perPage: 100 });
|
|
64
|
+
it('seedAll links belongsTo FK to a seeded parent', async () => {
|
|
65
|
+
const articles = await dbModule.findAll('article', {}, { perPage: 100 });
|
|
66
66
|
for (const art of articles.data) {
|
|
67
67
|
assert.ok(art.author_id !== null && art.author_id !== undefined);
|
|
68
68
|
}
|
|
@@ -74,12 +74,12 @@ describe('seeder', () => {
|
|
|
74
74
|
entities: { Tag: { properties: ['label'] } },
|
|
75
75
|
});
|
|
76
76
|
const defaultDbPath = path.join(os.tmpdir(), `chadstart-seed-default-${Date.now()}.db`);
|
|
77
|
-
dbModule.initDb(defaultCore, defaultDbPath);
|
|
77
|
+
await dbModule.initDb(defaultCore, defaultDbPath);
|
|
78
78
|
const result = await seedAll(defaultCore);
|
|
79
79
|
assert.strictEqual(result.summary.Tag, 50);
|
|
80
80
|
fs.unlinkSync(defaultDbPath);
|
|
81
81
|
// Restore the original seedCore DB for subsequent tests in this describe block
|
|
82
|
-
dbModule.initDb(seedCore, seedDbPath);
|
|
82
|
+
await dbModule.initDb(seedCore, seedDbPath);
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
it('seedAll creates admin@chadstart.com in authenticable entities', () => {
|
|
@@ -88,9 +88,9 @@ describe('seeder', () => {
|
|
|
88
88
|
assert.strictEqual(firstSeedResult.adminPassword, ADMIN_PASSWORD);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
it('seedAll creates admin user with correct email in the database', () => {
|
|
92
|
-
dbModule.initDb(seedCore, seedDbPath);
|
|
93
|
-
const admins = dbModule.findAllSimple('author', { email: ADMIN_EMAIL });
|
|
91
|
+
it('seedAll creates admin user with correct email in the database', async () => {
|
|
92
|
+
await dbModule.initDb(seedCore, seedDbPath);
|
|
93
|
+
const admins = await dbModule.findAllSimple('author', { email: ADMIN_EMAIL });
|
|
94
94
|
assert.strictEqual(admins.length, 1);
|
|
95
95
|
assert.strictEqual(admins[0].email, ADMIN_EMAIL);
|
|
96
96
|
});
|
|
@@ -107,10 +107,10 @@ describe('seeder', () => {
|
|
|
107
107
|
},
|
|
108
108
|
});
|
|
109
109
|
const freshDbPath = path.join(os.tmpdir(), `chadstart-seed-admin-${Date.now()}.db`);
|
|
110
|
-
dbModule.initDb(freshCore, freshDbPath);
|
|
110
|
+
await dbModule.initDb(freshCore, freshDbPath);
|
|
111
111
|
const result = await seedAll(freshCore);
|
|
112
112
|
assert.ok(result.adminEntities.includes('User'));
|
|
113
|
-
const admins = dbModule.findAllSimple('user', { email: ADMIN_EMAIL });
|
|
113
|
+
const admins = await dbModule.findAllSimple('user', { email: ADMIN_EMAIL });
|
|
114
114
|
assert.strictEqual(admins.length, 1);
|
|
115
115
|
assert.strictEqual(admins[0].email, ADMIN_EMAIL);
|
|
116
116
|
fs.unlinkSync(freshDbPath);
|
|
@@ -129,9 +129,9 @@ describe('seeder', () => {
|
|
|
129
129
|
},
|
|
130
130
|
});
|
|
131
131
|
const dupDbPath = path.join(os.tmpdir(), `chadstart-seed-dup-${Date.now()}.db`);
|
|
132
|
-
dbModule.initDb(dupCore, dupDbPath);
|
|
132
|
+
await dbModule.initDb(dupCore, dupDbPath);
|
|
133
133
|
// Manually create the admin user before seeding
|
|
134
|
-
dbModule.create('member', {
|
|
134
|
+
await dbModule.create('member', {
|
|
135
135
|
email: ADMIN_EMAIL,
|
|
136
136
|
password: bcrypt.hashSync(ADMIN_PASSWORD, 10),
|
|
137
137
|
name: 'pre-existing admin',
|
|
@@ -139,7 +139,7 @@ describe('seeder', () => {
|
|
|
139
139
|
// seedAll should not create a duplicate
|
|
140
140
|
const result = await seedAll(dupCore);
|
|
141
141
|
assert.strictEqual(result.adminEntities.length, 0);
|
|
142
|
-
const admins = dbModule.findAllSimple('member', { email: ADMIN_EMAIL });
|
|
142
|
+
const admins = await dbModule.findAllSimple('member', { email: ADMIN_EMAIL });
|
|
143
143
|
assert.strictEqual(admins.length, 1);
|
|
144
144
|
fs.unlinkSync(dupDbPath);
|
|
145
145
|
});
|
|
@@ -177,9 +177,9 @@ describe('seeder – property types', () => {
|
|
|
177
177
|
},
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
-
before(() => {
|
|
180
|
+
before(async () => {
|
|
181
181
|
tmp = path.join(os.tmpdir(), `chadstart-seedtypes-${Date.now()}.db`);
|
|
182
|
-
dbModule.initDb(core, tmp);
|
|
182
|
+
await dbModule.initDb(core, tmp);
|
|
183
183
|
});
|
|
184
184
|
|
|
185
185
|
after(() => { fs.unlinkSync(tmp); });
|
|
@@ -187,7 +187,7 @@ describe('seeder – property types', () => {
|
|
|
187
187
|
it('seedAll generates values for every property type', async () => {
|
|
188
188
|
const result = await seedAll(core);
|
|
189
189
|
assert.strictEqual(result.summary.Sample, 3);
|
|
190
|
-
const rows = dbModule.findAll('sample', {}, { perPage: 100 });
|
|
190
|
+
const rows = await dbModule.findAll('sample', {}, { perPage: 100 });
|
|
191
191
|
assert.strictEqual(rows.total, 3);
|
|
192
192
|
const r = rows.data[0];
|
|
193
193
|
assert.ok(typeof r.myText === 'string' && r.myText.length > 0);
|
|
@@ -217,7 +217,7 @@ describe('seeder – property types', () => {
|
|
|
217
217
|
entities: { Config: { single: true, properties: ['key', 'value'] } },
|
|
218
218
|
});
|
|
219
219
|
const singleTmp = path.join(os.tmpdir(), `chadstart-seedsingle-${Date.now()}.db`);
|
|
220
|
-
dbModule.initDb(singleCore, singleTmp);
|
|
220
|
+
await dbModule.initDb(singleCore, singleTmp);
|
|
221
221
|
const result = await seedAll(singleCore);
|
|
222
222
|
assert.strictEqual(result.summary.Config, 1);
|
|
223
223
|
fs.unlinkSync(singleTmp);
|
|
@@ -242,9 +242,9 @@ describe('seeder – authenticable entities with explicit email/password propert
|
|
|
242
242
|
},
|
|
243
243
|
});
|
|
244
244
|
|
|
245
|
-
before(() => {
|
|
245
|
+
before(async () => {
|
|
246
246
|
tmp = path.join(os.tmpdir(), `chadstart-authprop-${Date.now()}.db`);
|
|
247
|
-
dbModule.initDb(core, tmp);
|
|
247
|
+
await dbModule.initDb(core, tmp);
|
|
248
248
|
});
|
|
249
249
|
|
|
250
250
|
after(() => { fs.unlinkSync(tmp); });
|
|
@@ -263,7 +263,7 @@ describe('seeder – authenticable entities with explicit email/password propert
|
|
|
263
263
|
it('seedAll succeeds and creates records with valid email addresses', async () => {
|
|
264
264
|
const result = await seedAll(core);
|
|
265
265
|
assert.strictEqual(result.summary.Customer, 3);
|
|
266
|
-
const rows = dbModule.findAll('customer', {}, { perPage: 100 });
|
|
266
|
+
const rows = await dbModule.findAll('customer', {}, { perPage: 100 });
|
|
267
267
|
assert.ok(rows.total >= 3);
|
|
268
268
|
for (const r of rows.data) {
|
|
269
269
|
assert.ok(typeof r.email === 'string' && r.email.includes('@'), `email should contain @, got: ${r.email}`);
|
|
@@ -272,7 +272,7 @@ describe('seeder – authenticable entities with explicit email/password propert
|
|
|
272
272
|
});
|
|
273
273
|
|
|
274
274
|
it('seedAll creates admin user with correct email when entity has explicit email property', async () => {
|
|
275
|
-
const admins = dbModule.findAllSimple('customer', { email: ADMIN_EMAIL });
|
|
275
|
+
const admins = await dbModule.findAllSimple('customer', { email: ADMIN_EMAIL });
|
|
276
276
|
assert.strictEqual(admins.length, 1);
|
|
277
277
|
assert.strictEqual(admins[0].email, ADMIN_EMAIL);
|
|
278
278
|
});
|