@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,425 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as request from "supertest";
|
|
5
|
+
import { generateUniqueIdForObject } from "../../src/api";
|
|
6
|
+
import { buildAppForManager } from "../helpers/app";
|
|
7
|
+
import { baseConfig, fruitView } from "../helpers/fixtures";
|
|
8
|
+
import {
|
|
9
|
+
BackendName,
|
|
10
|
+
DbHarness,
|
|
11
|
+
describeIntegration,
|
|
12
|
+
getMysql2Harness,
|
|
13
|
+
getMysqlHarness,
|
|
14
|
+
getPgHarness,
|
|
15
|
+
resetTables,
|
|
16
|
+
} from "./dbHarness";
|
|
17
|
+
|
|
18
|
+
jest.mock("axios");
|
|
19
|
+
// Explicit factories: the file-based manual mocks in tests/__mocks__/ are not
|
|
20
|
+
// on the integration config's roots. They replicate the same CJS shapes.
|
|
21
|
+
jest.mock("@aws-sdk/client-s3", () => {
|
|
22
|
+
const send = jest.fn().mockResolvedValue({});
|
|
23
|
+
return {
|
|
24
|
+
S3Client: jest.fn(() => ({ send })),
|
|
25
|
+
PutObjectCommand: jest.fn((input: any) => ({ input })),
|
|
26
|
+
__send: send,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
jest.mock("@aws-sdk/s3-request-presigner", () => ({
|
|
30
|
+
getSignedUrl: jest
|
|
31
|
+
.fn()
|
|
32
|
+
.mockResolvedValue("https://signed.example.com/put-object"),
|
|
33
|
+
}));
|
|
34
|
+
jest.mock("@google-cloud/storage", () => {
|
|
35
|
+
const upload = jest.fn().mockResolvedValue([{}]);
|
|
36
|
+
const bucket = jest.fn(() => ({ upload }));
|
|
37
|
+
return {
|
|
38
|
+
Storage: jest.fn(() => ({ bucket })),
|
|
39
|
+
__bucket: bucket,
|
|
40
|
+
__upload: upload,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
jest.mock("ssh2-sftp-client", () => {
|
|
44
|
+
const connect = jest.fn().mockResolvedValue(undefined);
|
|
45
|
+
const fastPut = jest.fn().mockResolvedValue(undefined);
|
|
46
|
+
const end = jest.fn().mockResolvedValue(undefined);
|
|
47
|
+
const SftpClient: any = function () {
|
|
48
|
+
return { connect, fastPut, end };
|
|
49
|
+
};
|
|
50
|
+
SftpClient.__sftp = { connect, fastPut, end };
|
|
51
|
+
return SftpClient;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const PNG_BUFFER = Buffer.from(
|
|
55
|
+
"89504e470d0a1a0a0000000d49484452000000010000000108" + "0".repeat(64),
|
|
56
|
+
"hex",
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
function fileColumn(overrides: any = {}) {
|
|
60
|
+
return {
|
|
61
|
+
label: "Image",
|
|
62
|
+
name: "imageUrl",
|
|
63
|
+
dataType: "file",
|
|
64
|
+
...overrides,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Upload routes against real engines. The uploads themselves are external
|
|
70
|
+
* systems (S3 / GCP / SFTP / local fs), so those stay mocked; the real
|
|
71
|
+
* database provides the schema enrichment and the write-guard context.
|
|
72
|
+
*/
|
|
73
|
+
describeIntegration("upload routes against real databases", () => {
|
|
74
|
+
const harnesses: Record<BackendName, DbHarness> = {} as any;
|
|
75
|
+
|
|
76
|
+
beforeAll(async () => {
|
|
77
|
+
harnesses.mysql = await getMysqlHarness();
|
|
78
|
+
harnesses.mysql2 = await getMysql2Harness();
|
|
79
|
+
harnesses.postgresql = await getPgHarness();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
afterAll(async () => {
|
|
83
|
+
await harnesses.mysql?.stop();
|
|
84
|
+
await harnesses.mysql2?.stop();
|
|
85
|
+
await harnesses.postgresql?.stop();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe.each([
|
|
89
|
+
["mysql", () => harnesses.mysql],
|
|
90
|
+
["mysql2", () => harnesses.mysql2],
|
|
91
|
+
["postgresql", () => harnesses.postgresql],
|
|
92
|
+
])("backend: %s", (_backend, getHarness) => {
|
|
93
|
+
let harness: DbHarness;
|
|
94
|
+
|
|
95
|
+
beforeEach(async () => {
|
|
96
|
+
harness = getHarness();
|
|
97
|
+
await resetTables(harness);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const appWithColumn = (column: any, role: any = { role: "root", userId: 1 }) => {
|
|
101
|
+
const config = baseConfig({
|
|
102
|
+
views: [
|
|
103
|
+
fruitView({
|
|
104
|
+
primaryId: "id",
|
|
105
|
+
columns: [{ label: "Nom", name: "fruitName" }, column],
|
|
106
|
+
}),
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
const colId = generateUniqueIdForObject(column);
|
|
110
|
+
return { app: buildAppForManager(harness.manager, config, role), colId };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const s3Request = (app: any, colId: string) =>
|
|
114
|
+
request(app)
|
|
115
|
+
.post("/api/uploadFile")
|
|
116
|
+
.query({ viewIndex: 0, columnId: colId })
|
|
117
|
+
.attach("file", PNG_BUFFER, {
|
|
118
|
+
filename: "photo.png",
|
|
119
|
+
contentType: "image/png",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("S3 backend", () => {
|
|
123
|
+
it("uploads with a public-read PutObjectCommand and returns the public URL", async () => {
|
|
124
|
+
const s3Mock = jest.requireMock("@aws-sdk/client-s3");
|
|
125
|
+
const column = fileColumn({
|
|
126
|
+
s3Config: {
|
|
127
|
+
host: "s3.example.com",
|
|
128
|
+
accessKeyId: "AKIA",
|
|
129
|
+
secretAccessKey: "SECRET",
|
|
130
|
+
bucket: "my-bucket",
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const { app, colId } = appWithColumn(column);
|
|
134
|
+
const res = await s3Request(app, colId).expect(200);
|
|
135
|
+
|
|
136
|
+
expect(res.body.originalFilename).toBe("photo.png");
|
|
137
|
+
expect(res.body.aspectRatio).toBe(1); // PNG_BUFFER is a real 1x1 PNG header
|
|
138
|
+
expect(res.body.path).toMatch(/^photo-\d+\.png$/);
|
|
139
|
+
expect(res.body.url).toMatch(
|
|
140
|
+
/^https:\/\/s3\.example\.com\/my-bucket\/photo-\d+\.png$/,
|
|
141
|
+
);
|
|
142
|
+
expect(res.body.url).toBe(
|
|
143
|
+
`https://s3.example.com/my-bucket/${res.body.path}`,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(s3Mock.S3Client).toHaveBeenCalledWith(
|
|
147
|
+
expect.objectContaining({
|
|
148
|
+
endpoint: "https://s3.example.com",
|
|
149
|
+
region: undefined,
|
|
150
|
+
credentials: { accessKeyId: "AKIA", secretAccessKey: "SECRET" },
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
expect(s3Mock.PutObjectCommand).toHaveBeenCalledWith(
|
|
154
|
+
expect.objectContaining({
|
|
155
|
+
Bucket: "my-bucket",
|
|
156
|
+
ContentType: "image/png",
|
|
157
|
+
ACL: "public-read",
|
|
158
|
+
Key: res.body.path,
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
const sentCommand = s3Mock.PutObjectCommand.mock.calls[0][0];
|
|
162
|
+
expect(Buffer.isBuffer(sentCommand.Body)).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("prefixes https:// to hosts missing a scheme", async () => {
|
|
166
|
+
const s3Mock = jest.requireMock("@aws-sdk/client-s3");
|
|
167
|
+
const column = fileColumn({
|
|
168
|
+
s3Config: {
|
|
169
|
+
host: "s3.example.com",
|
|
170
|
+
accessKeyId: "k",
|
|
171
|
+
secretAccessKey: "s",
|
|
172
|
+
bucket: "b",
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
const { app, colId } = appWithColumn(column);
|
|
176
|
+
await s3Request(app, colId).expect(200);
|
|
177
|
+
expect(s3Mock.S3Client).toHaveBeenCalledWith(
|
|
178
|
+
expect.objectContaining({ endpoint: "https://s3.example.com" }),
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("keeps explicit http schemes untouched", async () => {
|
|
183
|
+
const s3Mock = jest.requireMock("@aws-sdk/client-s3");
|
|
184
|
+
const column = fileColumn({
|
|
185
|
+
s3Config: {
|
|
186
|
+
host: "http://minio.local:9000",
|
|
187
|
+
accessKeyId: "k",
|
|
188
|
+
secretAccessKey: "s",
|
|
189
|
+
bucket: "b",
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
const { app, colId } = appWithColumn(column);
|
|
193
|
+
await s3Request(app, colId).expect(200);
|
|
194
|
+
expect(s3Mock.S3Client).toHaveBeenCalledWith(
|
|
195
|
+
expect.objectContaining({ endpoint: "http://minio.local:9000" }),
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("uses the signed-URL flow when signedUrlNeeded is set", async () => {
|
|
200
|
+
const presignerMock = jest.requireMock("@aws-sdk/s3-request-presigner");
|
|
201
|
+
const axiosMock = jest.requireMock("axios");
|
|
202
|
+
const s3Mock = jest.requireMock("@aws-sdk/client-s3");
|
|
203
|
+
const column = fileColumn({
|
|
204
|
+
s3Config: {
|
|
205
|
+
host: "s3.example.com",
|
|
206
|
+
accessKeyId: "k",
|
|
207
|
+
secretAccessKey: "s",
|
|
208
|
+
bucket: "b",
|
|
209
|
+
signedUrlNeeded: true,
|
|
210
|
+
region: "eu-west-3",
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
const { app, colId } = appWithColumn(column);
|
|
214
|
+
const res = await s3Request(app, colId).expect(200);
|
|
215
|
+
|
|
216
|
+
expect(presignerMock.getSignedUrl).toHaveBeenCalledWith(
|
|
217
|
+
s3Mock.S3Client.mock.results[0].value,
|
|
218
|
+
expect.objectContaining({
|
|
219
|
+
input: expect.objectContaining({ Bucket: "b" }),
|
|
220
|
+
}),
|
|
221
|
+
{ expiresIn: 150 },
|
|
222
|
+
);
|
|
223
|
+
expect(axiosMock.put).toHaveBeenCalledWith(
|
|
224
|
+
"https://signed.example.com/put-object",
|
|
225
|
+
expect.any(Buffer),
|
|
226
|
+
expect.objectContaining({
|
|
227
|
+
headers: { "Content-Type": "image/png" },
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
// signed flow does not expose a public url
|
|
231
|
+
expect(res.body.url).toBeUndefined();
|
|
232
|
+
expect(res.body.path).toMatch(/^photo-\d+\.png$/);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("applies pathTransform when provided", async () => {
|
|
236
|
+
const column = fileColumn({
|
|
237
|
+
s3Config: {
|
|
238
|
+
host: "s3.example.com",
|
|
239
|
+
accessKeyId: "k",
|
|
240
|
+
secretAccessKey: "s",
|
|
241
|
+
bucket: "b",
|
|
242
|
+
pathTransform: (p: string) => `transformed/${p}`,
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
const { app, colId } = appWithColumn(column);
|
|
246
|
+
const res = await s3Request(app, colId).expect(200);
|
|
247
|
+
expect(res.body.path).toMatch(/^transformed\/photo-\d+\.png$/);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("supports a subfolder", async () => {
|
|
251
|
+
const column = fileColumn({
|
|
252
|
+
s3Config: {
|
|
253
|
+
host: "s3.example.com",
|
|
254
|
+
accessKeyId: "k",
|
|
255
|
+
secretAccessKey: "s",
|
|
256
|
+
bucket: "b",
|
|
257
|
+
subfolder: "images",
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
const { app, colId } = appWithColumn(column);
|
|
261
|
+
const res = await s3Request(app, colId).expect(200);
|
|
262
|
+
expect(res.body.path).toMatch(/^images\/photo-\d+\.png$/);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("returns 500 when the S3 upload fails", async () => {
|
|
266
|
+
const s3Mock = jest.requireMock("@aws-sdk/client-s3");
|
|
267
|
+
s3Mock.__send.mockRejectedValueOnce(new Error("s3 down"));
|
|
268
|
+
const column = fileColumn({
|
|
269
|
+
s3Config: {
|
|
270
|
+
host: "s3.example.com",
|
|
271
|
+
accessKeyId: "k",
|
|
272
|
+
secretAccessKey: "s",
|
|
273
|
+
bucket: "b",
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
const { app, colId } = appWithColumn(column);
|
|
277
|
+
await s3Request(app, colId).expect(500);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("GCP backend", () => {
|
|
282
|
+
it("uploads to the bucket and applies pathTransform", async () => {
|
|
283
|
+
const gcpMock = jest.requireMock("@google-cloud/storage");
|
|
284
|
+
const column = fileColumn({
|
|
285
|
+
gcpConfig: {
|
|
286
|
+
host: "storage.googleapis.com",
|
|
287
|
+
bucket: "gcp-bucket",
|
|
288
|
+
pathTransform: (p: string) => `cdn/${p}`,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
const { app, colId } = appWithColumn(column);
|
|
292
|
+
const res = await s3Request(app, colId).expect(200);
|
|
293
|
+
|
|
294
|
+
expect(gcpMock.Storage).toHaveBeenCalled();
|
|
295
|
+
expect(gcpMock.__bucket).toHaveBeenCalledWith("gcp-bucket");
|
|
296
|
+
expect(gcpMock.__upload).toHaveBeenCalledWith(
|
|
297
|
+
expect.any(String),
|
|
298
|
+
expect.objectContaining({
|
|
299
|
+
destination: expect.stringMatching(/^photo-\d+\.png$/),
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
expect(res.body.path).toMatch(/^cdn\/photo-\d+\.png$/);
|
|
303
|
+
expect(res.body.aspectRatio).toBe(1); // PNG_BUFFER is a real 1x1 PNG header
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("returns 500 when the upload fails", async () => {
|
|
307
|
+
const gcpMock = jest.requireMock("@google-cloud/storage");
|
|
308
|
+
gcpMock.__upload.mockRejectedValueOnce(new Error("gcp down"));
|
|
309
|
+
const column = fileColumn({
|
|
310
|
+
gcpConfig: { host: "storage.googleapis.com", bucket: "gcp-bucket" },
|
|
311
|
+
});
|
|
312
|
+
const { app, colId } = appWithColumn(column);
|
|
313
|
+
await s3Request(app, colId).expect(500);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("SFTP backend", () => {
|
|
318
|
+
it("requires publicPath", async () => {
|
|
319
|
+
const column = fileColumn({
|
|
320
|
+
sftpConfig: { host: "h", user: "u", password: "p" },
|
|
321
|
+
});
|
|
322
|
+
const { app, colId } = appWithColumn(column);
|
|
323
|
+
const res = await s3Request(app, colId).expect(500);
|
|
324
|
+
expect(res.text).toContain("publicPath");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("fastPuts the temp file to the remote path and cleans it up", async () => {
|
|
328
|
+
const sftpMock = jest.requireMock("ssh2-sftp-client");
|
|
329
|
+
const column = fileColumn({
|
|
330
|
+
publicPath: "/srv/public",
|
|
331
|
+
subdirectory: "img",
|
|
332
|
+
sftpConfig: { host: "sftp.example.com", user: "u", password: "p" },
|
|
333
|
+
});
|
|
334
|
+
const { app, colId } = appWithColumn(column);
|
|
335
|
+
const res = await s3Request(app, colId).expect(200);
|
|
336
|
+
|
|
337
|
+
expect(sftpMock.__sftp.connect).toHaveBeenCalledWith({
|
|
338
|
+
host: "sftp.example.com",
|
|
339
|
+
port: 22,
|
|
340
|
+
username: "u",
|
|
341
|
+
password: "p",
|
|
342
|
+
});
|
|
343
|
+
const [localPath, remotePath] = sftpMock.__sftp.fastPut.mock.calls[0];
|
|
344
|
+
expect(path.dirname(remotePath)).toBe("/srv/public/img");
|
|
345
|
+
expect(path.basename(remotePath)).toBe(path.basename(localPath));
|
|
346
|
+
expect(res.body).toEqual({
|
|
347
|
+
path: path.basename(remotePath),
|
|
348
|
+
originalFilename: "photo.png",
|
|
349
|
+
});
|
|
350
|
+
// the temp file is removed after a successful transfer
|
|
351
|
+
expect(fs.existsSync(localPath)).toBe(false);
|
|
352
|
+
expect(sftpMock.__sftp.end).toHaveBeenCalled();
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("local fs backend", () => {
|
|
357
|
+
let uploadRoot: string;
|
|
358
|
+
|
|
359
|
+
beforeEach(() => {
|
|
360
|
+
uploadRoot = fs.mkdtempSync(path.join(os.tmpdir(), "galadmin-upload-"));
|
|
361
|
+
fs.mkdirSync(path.join(uploadRoot, "sub"));
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
afterEach(() => {
|
|
365
|
+
fs.rmSync(uploadRoot, { recursive: true, force: true });
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("stores the file under publicPath + subdirectory and returns the relative path", async () => {
|
|
369
|
+
const column = fileColumn({
|
|
370
|
+
publicPath: uploadRoot,
|
|
371
|
+
subdirectory: "/sub",
|
|
372
|
+
});
|
|
373
|
+
const { app, colId } = appWithColumn(column);
|
|
374
|
+
const res = await s3Request(app, colId).expect(200);
|
|
375
|
+
// the local backend keeps multiparty's random temp filename (not the
|
|
376
|
+
// original one) — documented behavior
|
|
377
|
+
expect(res.body.path).toMatch(/^\/sub\/[A-Za-z0-9_-]+\.png$/);
|
|
378
|
+
expect(res.body.originalFilename).toBe("photo.png");
|
|
379
|
+
const stored = path.join(uploadRoot, res.body.path);
|
|
380
|
+
expect(fs.existsSync(stored)).toBe(true);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("fails without a subdirectory (publicPath + undefined concat — documented quirk)", async () => {
|
|
384
|
+
// uploadDir becomes "<publicPath>undefined", which never exists, so
|
|
385
|
+
// the multipart parse fails with 500. A subdirectory is effectively
|
|
386
|
+
// required for the local backend.
|
|
387
|
+
const column = fileColumn({ publicPath: uploadRoot });
|
|
388
|
+
const { app, colId } = appWithColumn(column);
|
|
389
|
+
await s3Request(app, colId).expect(500);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe("POST /api/uploadFileFromBetterRichTextInput", () => {
|
|
394
|
+
it("takes the upload config from the file-upload-properties header", async () => {
|
|
395
|
+
const s3Mock = jest.requireMock("@aws-sdk/client-s3");
|
|
396
|
+
const app = buildAppForManager(
|
|
397
|
+
harness.manager,
|
|
398
|
+
baseConfig({ views: [fruitView({ primaryId: "id" })] }),
|
|
399
|
+
);
|
|
400
|
+
const res = await request(app)
|
|
401
|
+
.post("/api/uploadFileFromBetterRichTextInput")
|
|
402
|
+
.set(
|
|
403
|
+
"file-upload-properties",
|
|
404
|
+
JSON.stringify({
|
|
405
|
+
s3Config: {
|
|
406
|
+
host: "s3.example.com",
|
|
407
|
+
accessKeyId: "k",
|
|
408
|
+
secretAccessKey: "s",
|
|
409
|
+
bucket: "b",
|
|
410
|
+
},
|
|
411
|
+
}),
|
|
412
|
+
)
|
|
413
|
+
.attach("upload", PNG_BUFFER, {
|
|
414
|
+
filename: "photo.png",
|
|
415
|
+
contentType: "image/png",
|
|
416
|
+
})
|
|
417
|
+
.expect(200);
|
|
418
|
+
expect(s3Mock.PutObjectCommand).toHaveBeenCalledWith(
|
|
419
|
+
expect.objectContaining({ Bucket: "b" }),
|
|
420
|
+
);
|
|
421
|
+
expect(res.body.path).toMatch(/^photo-\d+\.png$/);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const noop = () => {};
|
|
2
|
+
|
|
3
|
+
const originalError = console.error;
|
|
4
|
+
const originalLog = console.log;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
if (process.env.TEST_VERBOSE) return;
|
|
8
|
+
console.error = noop;
|
|
9
|
+
console.log = noop;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
console.error = originalError;
|
|
14
|
+
console.log = originalLog;
|
|
15
|
+
});
|