@galadrim/galadmin 0.1.236 → 0.2.7
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/main.yml +34 -3
- package/README.md +65 -0
- package/dist/api.d.ts +16 -0
- package/dist/api.js +240 -189
- package/dist/api.js.map +1 -1
- package/dist/index.js +25 -3
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/config.d.ts +6 -1
- package/dist/utils/dbManager.d.ts +5 -0
- package/dist/utils/dbManager.js +146 -11
- package/dist/utils/dbManager.js.map +1 -1
- package/dist/utils/imageDimensions.d.ts +5 -0
- package/dist/utils/imageDimensions.js +98 -0
- package/dist/utils/imageDimensions.js.map +1 -0
- package/jest.config.js +43 -0
- package/jest.integration.config.js +16 -0
- package/package.json +30 -10
- package/src/api.ts +284 -217
- package/src/front/src/App.test.js +28 -0
- package/src/front/src/components/OutsideClickHandler.test.js +53 -0
- package/src/front/src/components/RedirectToFirstView.test.js +38 -0
- package/src/front/src/utils/checkRequiredColumns.test.js +46 -0
- package/src/front/src/utils/evalExpression.test.js +29 -0
- package/src/index.ts +26 -3
- package/src/types/config.ts +13 -1
- package/src/utils/dbManager.ts +243 -102
- package/src/utils/imageDimensions.ts +112 -0
- package/tests/__mocks__/@aws-sdk/client-s3.js +6 -0
- package/tests/__mocks__/@aws-sdk/s3-request-presigner.js +3 -0
- package/tests/__mocks__/@google-cloud/storage.js +7 -0
- package/tests/__mocks__/argon2.js +3 -0
- package/tests/__mocks__/axios.js +7 -0
- package/tests/__mocks__/connect-pg-simple.js +24 -0
- package/tests/__mocks__/express-mysql-session.js +24 -0
- package/tests/__mocks__/mysql2/promise.js +3 -0
- package/tests/__mocks__/pg.js +3 -0
- package/tests/__mocks__/rollup.js +5 -0
- package/tests/__mocks__/ssh2-sftp-client.js +10 -0
- package/tests/api-helpers.test.ts +376 -0
- package/tests/dbManager.init.test.ts +129 -0
- package/tests/dbManager.test.ts +265 -0
- package/tests/edge-cases.test.ts +167 -0
- package/tests/helpers/app.ts +69 -0
- package/tests/helpers/fakeDb.ts +66 -0
- package/tests/helpers/fakePool.ts +76 -0
- package/tests/helpers/fixtures.ts +68 -0
- package/tests/imageDimensions.test.ts +111 -0
- package/tests/index.sessionStore.test.ts +19 -0
- package/tests/index.test.ts +412 -0
- package/tests/integration/__mocks__/axios.js +3 -0
- package/tests/integration/dbHarness.ts +381 -0
- package/tests/integration/dbManager.integration.test.ts +69 -0
- package/tests/integration/globalSetup.ts +43 -0
- package/tests/integration/globalTeardown.ts +42 -0
- package/tests/integration/routes.authorization.integration.test.ts +319 -0
- package/tests/integration/routes.config.integration.test.ts +189 -0
- package/tests/integration/routes.crud.integration.test.ts +805 -0
- package/tests/integration/routes.custom.integration.test.ts +376 -0
- package/tests/integration/routes.foreign.integration.test.ts +306 -0
- package/tests/integration/routes.importExport.integration.test.ts +404 -0
- package/tests/integration/routes.integration.test.ts +366 -0
- package/tests/integration/routes.postgresql.integration.test.ts +88 -0
- package/tests/integration/routes.records.integration.test.ts +634 -0
- package/tests/integration/routes.upload.integration.test.ts +425 -0
- package/tests/setup/silence-console.ts +15 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import * as request from "supertest";
|
|
2
|
+
import { buildAppForManager } from "../helpers/app";
|
|
3
|
+
import { baseConfig, fruitView, EDITOR, VIEWER } from "../helpers/fixtures";
|
|
4
|
+
import {
|
|
5
|
+
BackendName,
|
|
6
|
+
DbHarness,
|
|
7
|
+
describeIntegration,
|
|
8
|
+
getMysql2Harness,
|
|
9
|
+
getMysqlHarness,
|
|
10
|
+
getPgHarness,
|
|
11
|
+
resetTables,
|
|
12
|
+
} from "./dbHarness";
|
|
13
|
+
|
|
14
|
+
jest.mock("axios");
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* /recordsExport, /recordsImport and /subrecordsImport against real engines.
|
|
18
|
+
* The scripted suite asserted generated SQL strings; here exports are checked
|
|
19
|
+
* by their CSV output and imports by the rows the engine actually holds.
|
|
20
|
+
*/
|
|
21
|
+
describeIntegration("import/export routes against real databases", () => {
|
|
22
|
+
const harnesses: Record<BackendName, DbHarness> = {} as any;
|
|
23
|
+
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
harnesses.mysql = await getMysqlHarness();
|
|
26
|
+
harnesses.mysql2 = await getMysql2Harness();
|
|
27
|
+
harnesses.postgresql = await getPgHarness();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterAll(async () => {
|
|
31
|
+
await harnesses.mysql?.stop();
|
|
32
|
+
await harnesses.mysql2?.stop();
|
|
33
|
+
await harnesses.postgresql?.stop();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe.each([
|
|
37
|
+
["mysql", () => harnesses.mysql],
|
|
38
|
+
["mysql2", () => harnesses.mysql2],
|
|
39
|
+
["postgresql", () => harnesses.postgresql],
|
|
40
|
+
])("backend: %s", (_backend, getHarness) => {
|
|
41
|
+
let harness: DbHarness;
|
|
42
|
+
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
harness = getHarness();
|
|
45
|
+
await resetTables(harness);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const dataLines = (content: string) => content.split("\r\n").slice(1);
|
|
49
|
+
|
|
50
|
+
const seedFruits = async (rows: Array<Record<string, any>>) => {
|
|
51
|
+
for (const r of rows) {
|
|
52
|
+
const { err } = await harness.query(
|
|
53
|
+
"INSERT INTO fruits (fruitName, acidity, note, age, ownerId) VALUES (?, ?, ?, ?, ?)",
|
|
54
|
+
[
|
|
55
|
+
r.fruitName ?? null,
|
|
56
|
+
r.acidity ?? null,
|
|
57
|
+
r.note ?? null,
|
|
58
|
+
r.age ?? null,
|
|
59
|
+
r.ownerId ?? null,
|
|
60
|
+
],
|
|
61
|
+
);
|
|
62
|
+
expect(err).toBeFalsy();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
describe("POST /api/recordsExport", () => {
|
|
67
|
+
const exportConfig = (viewOverrides: any = {}) =>
|
|
68
|
+
baseConfig({
|
|
69
|
+
views: [
|
|
70
|
+
fruitView({
|
|
71
|
+
primaryId: "id",
|
|
72
|
+
columns: [
|
|
73
|
+
{ label: "Nom", name: "fruitName" },
|
|
74
|
+
{ label: "Acidité", name: "acidity" },
|
|
75
|
+
{
|
|
76
|
+
label: "Notes",
|
|
77
|
+
name: "note",
|
|
78
|
+
dataType: "textarea",
|
|
79
|
+
hideInTable: true,
|
|
80
|
+
includeInExport: true,
|
|
81
|
+
},
|
|
82
|
+
{ label: "Caché", name: "hidden", hideInTable: true },
|
|
83
|
+
{ label: "Vendu", name: "age", dataType: "tinyint" },
|
|
84
|
+
],
|
|
85
|
+
...viewOverrides,
|
|
86
|
+
}),
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const postExport = (instance: any, body: any = {}) =>
|
|
91
|
+
request(instance)
|
|
92
|
+
.post("/api/recordsExport")
|
|
93
|
+
.send({ viewIndex: 0, filters: [], ...body });
|
|
94
|
+
|
|
95
|
+
it("exports a semicolon-delimited CSV with header and formatted rows", async () => {
|
|
96
|
+
await seedFruits([
|
|
97
|
+
{ fruitName: "pomme", acidity: 3, note: "bonne", age: 1 },
|
|
98
|
+
]);
|
|
99
|
+
const res = await postExport(
|
|
100
|
+
buildAppForManager(harness.manager, exportConfig()),
|
|
101
|
+
).expect(200);
|
|
102
|
+
const lines = res.body.content.split("\r\n");
|
|
103
|
+
expect(lines[0]).toBe("Nom;Acidité;Notes;Vendu");
|
|
104
|
+
expect(lines[1]).toBe("pomme;3;\"bonne\";Oui");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("clamps the limit to 999999 (allRecords)", async () => {
|
|
108
|
+
await seedFruits(
|
|
109
|
+
Array.from({ length: 12 }, (_, i) => ({ fruitName: `f${i + 1}` })),
|
|
110
|
+
);
|
|
111
|
+
const instance = buildAppForManager(
|
|
112
|
+
harness.manager,
|
|
113
|
+
exportConfig({ limit: 5 }),
|
|
114
|
+
);
|
|
115
|
+
const res = await postExport(instance).expect(200);
|
|
116
|
+
// allRecords overrides the view limit: every row is exported.
|
|
117
|
+
expect(dataLines(res.body.content)).toHaveLength(12);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("applies orderBy with direction", async () => {
|
|
121
|
+
await seedFruits([
|
|
122
|
+
{ fruitName: "a", acidity: 1 },
|
|
123
|
+
{ fruitName: "b", acidity: 3 },
|
|
124
|
+
{ fruitName: "c", acidity: 2 },
|
|
125
|
+
]);
|
|
126
|
+
const res = await postExport(
|
|
127
|
+
buildAppForManager(harness.manager, exportConfig()),
|
|
128
|
+
{ orderBy: "acidity", isDesc: true },
|
|
129
|
+
).expect(200);
|
|
130
|
+
expect(dataLines(res.body.content)[0]).toMatch(/^b;/);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("applies view filters, subview filters and body filters", async () => {
|
|
134
|
+
await seedFruits([
|
|
135
|
+
{ fruitName: "keep", age: 30, acidity: 2 },
|
|
136
|
+
{ fruitName: "too-young", age: 5, acidity: 5 },
|
|
137
|
+
{ fruitName: "too-old", age: 60, acidity: 5 },
|
|
138
|
+
{ fruitName: "too-sour", age: 30, acidity: 0 },
|
|
139
|
+
]);
|
|
140
|
+
const config = exportConfig({
|
|
141
|
+
filters: "age > 10",
|
|
142
|
+
subviews: [{ label: "Old", filters: "age < 50" }],
|
|
143
|
+
});
|
|
144
|
+
const res = await postExport(
|
|
145
|
+
buildAppForManager(harness.manager, config),
|
|
146
|
+
{ subviewIndex: 0, filters: "acidity > 1" },
|
|
147
|
+
).expect(200);
|
|
148
|
+
expect(dataLines(res.body.content)).toHaveLength(1);
|
|
149
|
+
expect(dataLines(res.body.content)[0]).toMatch(/^keep;/);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("scopes the export to the caller's rows via filtersFromUser", async () => {
|
|
153
|
+
// The export must be scoped exactly like the list view, otherwise a
|
|
154
|
+
// user could download rows their filtersFromUser clause hides.
|
|
155
|
+
await seedFruits([
|
|
156
|
+
{ fruitName: "mine", ownerId: 2 },
|
|
157
|
+
{ fruitName: "theirs", ownerId: 1 },
|
|
158
|
+
]);
|
|
159
|
+
const config = exportConfig({
|
|
160
|
+
filtersFromUser: (userId: number) => `ownerId = ${userId}`,
|
|
161
|
+
});
|
|
162
|
+
const res = await postExport(
|
|
163
|
+
buildAppForManager(harness.manager, config, {
|
|
164
|
+
role: "editor",
|
|
165
|
+
userId: 2,
|
|
166
|
+
}),
|
|
167
|
+
).expect(200);
|
|
168
|
+
expect(dataLines(res.body.content)).toHaveLength(1);
|
|
169
|
+
expect(dataLines(res.body.content)[0]).toMatch(/^mine;/);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("returns 500 when the engine rejects the query", async () => {
|
|
173
|
+
const config = exportConfig({ filters: "price < 10" });
|
|
174
|
+
await postExport(buildAppForManager(harness.manager, config)).expect(
|
|
175
|
+
500,
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("renders null values as empty strings", async () => {
|
|
180
|
+
await seedFruits([{ fruitName: null, acidity: null, note: null, age: 0 }]);
|
|
181
|
+
const res = await postExport(
|
|
182
|
+
buildAppForManager(harness.manager, exportConfig()),
|
|
183
|
+
).expect(200);
|
|
184
|
+
expect(dataLines(res.body.content)[0]).toBe(";;\"\";Non");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("POST /api/recordsImport", () => {
|
|
189
|
+
const importConfig = (overrides: any = {}) =>
|
|
190
|
+
baseConfig({
|
|
191
|
+
views: [
|
|
192
|
+
fruitView({
|
|
193
|
+
primaryId: "id",
|
|
194
|
+
parseImportFile: jest.fn().mockResolvedValue([
|
|
195
|
+
{ id: 1, fruitName: "pomme" },
|
|
196
|
+
{ id: 2, fruitName: "poire" },
|
|
197
|
+
]),
|
|
198
|
+
importFileUniqueField: "id",
|
|
199
|
+
...overrides,
|
|
200
|
+
}),
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const postImport = (instance: any) =>
|
|
205
|
+
request(instance)
|
|
206
|
+
.post("/api/recordsImport")
|
|
207
|
+
.field("viewIndex", "0")
|
|
208
|
+
.attach("data", Buffer.from("fake,csv"), "import.csv");
|
|
209
|
+
|
|
210
|
+
it("bulk upserts the parsed records on conflict", async () => {
|
|
211
|
+
// id 1 already exists: the import must update it, not duplicate it.
|
|
212
|
+
const { err } = await harness.query(
|
|
213
|
+
"INSERT INTO fruits (id, fruitName) VALUES (1, 'old')",
|
|
214
|
+
);
|
|
215
|
+
expect(err).toBeFalsy();
|
|
216
|
+
const config = importConfig();
|
|
217
|
+
const res = await postImport(
|
|
218
|
+
buildAppForManager(harness.manager, config),
|
|
219
|
+
).expect(200);
|
|
220
|
+
// MySQL reports 1 per inserted row and 2 per updated one (1+2=3).
|
|
221
|
+
if (harness.dbType === "POSTGRESQL") {
|
|
222
|
+
// GAP: the PG driver result exposes rowCount, not affectedRows,
|
|
223
|
+
// so the route answers with an empty object.
|
|
224
|
+
expect(res.body).toEqual({});
|
|
225
|
+
} else {
|
|
226
|
+
expect(res.body).toEqual({ affectedRows: 3 });
|
|
227
|
+
}
|
|
228
|
+
expect(config.views[0].parseImportFile).toHaveBeenCalledWith(
|
|
229
|
+
expect.objectContaining({ originalFilename: "import.csv" }),
|
|
230
|
+
);
|
|
231
|
+
const { rows } = await harness.query(
|
|
232
|
+
"SELECT id, fruitName FROM fruits ORDER BY id",
|
|
233
|
+
);
|
|
234
|
+
expect(rows).toEqual([
|
|
235
|
+
{ id: 1, fruitName: "pomme" },
|
|
236
|
+
{ id: 2, fruitName: "poire" },
|
|
237
|
+
]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("returns 0 when the import file holds no records", async () => {
|
|
241
|
+
const config = importConfig({
|
|
242
|
+
parseImportFile: jest.fn().mockResolvedValue([]),
|
|
243
|
+
});
|
|
244
|
+
const res = await postImport(
|
|
245
|
+
buildAppForManager(harness.manager, config),
|
|
246
|
+
).expect(200);
|
|
247
|
+
expect(res.body).toEqual({ affectedRows: 0 });
|
|
248
|
+
const { rows } = await harness.query("SELECT * FROM fruits");
|
|
249
|
+
expect(rows).toHaveLength(0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("returns 500 when parseImportFile is missing", async () => {
|
|
253
|
+
const config = importConfig();
|
|
254
|
+
delete (config.views[0] as any).parseImportFile;
|
|
255
|
+
const res = await postImport(
|
|
256
|
+
buildAppForManager(harness.manager, config),
|
|
257
|
+
).expect(500);
|
|
258
|
+
expect(res.text).toContain("parseImportFile");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("rejects a read-only role with 403 before parsing or importing", async () => {
|
|
262
|
+
// Imports are bulk writes: they require a role that can write the view.
|
|
263
|
+
const config = importConfig({
|
|
264
|
+
roles: ["editor"],
|
|
265
|
+
readRoles: ["viewer"],
|
|
266
|
+
});
|
|
267
|
+
await postImport(
|
|
268
|
+
buildAppForManager(harness.manager, config, VIEWER),
|
|
269
|
+
).expect(403);
|
|
270
|
+
expect(config.views[0].parseImportFile).not.toHaveBeenCalled();
|
|
271
|
+
const { rows } = await harness.query("SELECT * FROM fruits");
|
|
272
|
+
expect(rows).toHaveLength(0);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// NOTE: a throwing parseImportFile is awaited OUTSIDE the try/catch of
|
|
276
|
+
// the multiparty callback -> unhandled rejection, the 500 is never sent
|
|
277
|
+
// and the process can crash. Listed in README under "Known robustness
|
|
278
|
+
// limitations".
|
|
279
|
+
|
|
280
|
+
it("returns 500 with a message when the insert fails", async () => {
|
|
281
|
+
const config = importConfig({
|
|
282
|
+
parseImportFile: jest
|
|
283
|
+
.fn()
|
|
284
|
+
.mockResolvedValue([{ id: 1, nope: "x" }]),
|
|
285
|
+
});
|
|
286
|
+
// The mapped column "nope" does not exist: engine error -> 500.
|
|
287
|
+
const res = await postImport(
|
|
288
|
+
buildAppForManager(harness.manager, config),
|
|
289
|
+
).expect(500);
|
|
290
|
+
expect(res.body.message).toBe("Une erreur est survenue.");
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("POST /api/subrecordsImport", () => {
|
|
295
|
+
const panelConfig = (overrides: any = {}) => {
|
|
296
|
+
const panel = {
|
|
297
|
+
label: "Notes",
|
|
298
|
+
tableName: "fruit_notes",
|
|
299
|
+
primaryId: "id",
|
|
300
|
+
createConfig: {
|
|
301
|
+
label: "New note",
|
|
302
|
+
recordIdKey: "fruitId",
|
|
303
|
+
columns: [{ label: "Texte", name: "text" }],
|
|
304
|
+
},
|
|
305
|
+
parseImportFile: jest
|
|
306
|
+
.fn()
|
|
307
|
+
.mockResolvedValue([{ text: "note a" }, { text: "note b" }]),
|
|
308
|
+
...overrides,
|
|
309
|
+
};
|
|
310
|
+
return baseConfig({ views: [fruitView({ recordViewPanels: [panel] })] });
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const postSubImport = (instance: any) =>
|
|
314
|
+
request(instance)
|
|
315
|
+
.post("/api/subrecordsImport")
|
|
316
|
+
.field("viewIndex", "0")
|
|
317
|
+
.field("panelIndex", "0")
|
|
318
|
+
.field("recordId", "7")
|
|
319
|
+
.attach("data", Buffer.from("fake,csv"), "notes.csv");
|
|
320
|
+
|
|
321
|
+
it("bulk upserts panel records bound to the parent record", async () => {
|
|
322
|
+
const config = panelConfig();
|
|
323
|
+
const res = await postSubImport(
|
|
324
|
+
buildAppForManager(harness.manager, config),
|
|
325
|
+
);
|
|
326
|
+
if (harness.dbType === "POSTGRESQL") {
|
|
327
|
+
// The MySQL-only INSERT IGNORE syntax fails on PG — see the
|
|
328
|
+
// characterization test below.
|
|
329
|
+
expect(res.status).toBe(500);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
expect(res.status).toBe(200);
|
|
333
|
+
expect(res.body).toEqual({ affectedRows: 2 });
|
|
334
|
+
expect(
|
|
335
|
+
config.views[0].recordViewPanels[0].parseImportFile,
|
|
336
|
+
).toHaveBeenCalledWith(
|
|
337
|
+
expect.objectContaining({ originalFilename: "notes.csv" }),
|
|
338
|
+
"7",
|
|
339
|
+
);
|
|
340
|
+
const { rows } = await harness.query(
|
|
341
|
+
"SELECT fruitId, text FROM fruit_notes ORDER BY id",
|
|
342
|
+
);
|
|
343
|
+
expect(rows).toEqual([
|
|
344
|
+
{ fruitId: 7, text: "note a" },
|
|
345
|
+
{ fruitId: 7, text: "note b" },
|
|
346
|
+
]);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("CHARACTERIZATION: PostgreSQL rejects the MySQL-only INSERT IGNORE syntax", async () => {
|
|
350
|
+
// The panel import always builds INSERT IGNORE ... ON DUPLICATE KEY
|
|
351
|
+
// UPDATE regardless of dialect; convertQuery quotes the unknown
|
|
352
|
+
// keywords instead of translating them, so PG answers 500.
|
|
353
|
+
const res = await postSubImport(
|
|
354
|
+
buildAppForManager(harness.manager, panelConfig()),
|
|
355
|
+
);
|
|
356
|
+
if (harness.dbType === "POSTGRESQL") {
|
|
357
|
+
expect(res.status).toBe(500);
|
|
358
|
+
} else {
|
|
359
|
+
expect(res.status).toBe(200);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("returns 0 when the file holds no records", async () => {
|
|
364
|
+
const config = panelConfig({
|
|
365
|
+
parseImportFile: jest.fn().mockResolvedValue([]),
|
|
366
|
+
});
|
|
367
|
+
const res = await postSubImport(
|
|
368
|
+
buildAppForManager(harness.manager, config),
|
|
369
|
+
).expect(200);
|
|
370
|
+
expect(res.body).toEqual({ affectedRows: 0 });
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("returns 500 when parseImportFile is missing", async () => {
|
|
374
|
+
const config = panelConfig();
|
|
375
|
+
delete (config.views[0].recordViewPanels[0] as any).parseImportFile;
|
|
376
|
+
const res = await postSubImport(
|
|
377
|
+
buildAppForManager(harness.manager, config),
|
|
378
|
+
).expect(500);
|
|
379
|
+
expect(res.text).toContain("parseImportFile");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("returns 500 when createConfig is missing", async () => {
|
|
383
|
+
const config = panelConfig();
|
|
384
|
+
delete (config.views[0].recordViewPanels[0] as any).createConfig;
|
|
385
|
+
const res = await postSubImport(
|
|
386
|
+
buildAppForManager(harness.manager, config),
|
|
387
|
+
).expect(500);
|
|
388
|
+
expect(res.text).toContain("createConfig");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("rejects a read-only role with 403 before importing", async () => {
|
|
392
|
+
const config = panelConfig({ roles: ["editor"] });
|
|
393
|
+
await postSubImport(
|
|
394
|
+
buildAppForManager(harness.manager, config, VIEWER),
|
|
395
|
+
).expect(403);
|
|
396
|
+
expect(
|
|
397
|
+
config.views[0].recordViewPanels[0].parseImportFile,
|
|
398
|
+
).not.toHaveBeenCalled();
|
|
399
|
+
const { rows } = await harness.query("SELECT * FROM fruit_notes");
|
|
400
|
+
expect(rows).toHaveLength(0);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
});
|