-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathupload-resolver.e2e.test.ts
More file actions
239 lines (208 loc) · 6.6 KB
/
upload-resolver.e2e.test.ts
File metadata and controls
239 lines (208 loc) · 6.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
import { S3StorageProvider } from '@constructive-io/s3-streamer';
import { Client as PgClient } from 'pg';
import { Readable } from 'stream';
jest.setTimeout(60000);
const SCHEMA = 'files_store_public';
const TABLE = 'files';
const BUCKET = 'test-bucket';
const USER_ID = 'aaaaaaaa-0000-0000-0000-000000000001';
const MINIMAL_PNG = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO6xM4cAAAAASUVORK5CYII=',
'base64',
);
type UploadResolverModule = typeof import('../src/upload-resolver');
function makePg(): PgClient {
return new PgClient({
host: 'localhost',
port: 5432,
user: 'postgres',
password: 'password',
database: 'constructive',
});
}
function makeStorage(): S3StorageProvider {
return new S3StorageProvider({
bucket: BUCKET,
awsRegion: 'us-east-1',
awsAccessKey: 'minioadmin',
awsSecretKey: 'minioadmin',
minioEndpoint: 'http://localhost:9000',
provider: 'minio',
});
}
async function setupFilesStoreSchema(pg: PgClient): Promise<void> {
await pg.query('CREATE EXTENSION IF NOT EXISTS pgcrypto');
await pg.query(`CREATE SCHEMA IF NOT EXISTS ${SCHEMA}`);
await pg.query(`
DO $$ BEGIN
CREATE TYPE ${SCHEMA}.file_status AS ENUM (
'pending', 'processing', 'ready', 'error', 'deleting'
);
EXCEPTION WHEN duplicate_object THEN NULL;
END $$
`);
await pg.query(`
CREATE TABLE IF NOT EXISTS ${SCHEMA}.${TABLE} (
id uuid NOT NULL DEFAULT gen_random_uuid(),
database_id integer NOT NULL,
bucket_key text NOT NULL DEFAULT 'default',
key text NOT NULL,
status ${SCHEMA}.file_status NOT NULL DEFAULT 'pending',
status_reason text,
etag text,
source_table text,
source_column text,
source_id uuid,
processing_started_at timestamptz,
created_by uuid,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT graphile_settings_files_store_files_pkey PRIMARY KEY (id, database_id)
)
`);
}
async function cleanupFilesStoreRows(pg: PgClient): Promise<void> {
await pg.query(`DELETE FROM ${SCHEMA}.${TABLE}`);
}
async function objectExists(storage: S3StorageProvider, key: string): Promise<boolean> {
try {
await storage.head(key);
return true;
} catch {
return false;
}
}
async function loadUploadResolverModule(): Promise<UploadResolverModule> {
jest.resetModules();
return import('../src/upload-resolver');
}
function makeUpload(filename: string, body: Buffer) {
return {
filename,
createReadStream: () => Readable.from(body),
};
}
describe('upload-resolver e2e', () => {
let pg: PgClient;
let storage: S3StorageProvider;
let uploadResolverModule: UploadResolverModule | null = null;
const originalEnv = { ...process.env };
const uploadedKeys = new Set<string>();
beforeAll(async () => {
process.env.BUCKET_PROVIDER = 'minio';
process.env.BUCKET_NAME = BUCKET;
process.env.AWS_REGION = 'us-east-1';
process.env.AWS_ACCESS_KEY = 'minioadmin';
process.env.AWS_SECRET_KEY = 'minioadmin';
process.env.MINIO_ENDPOINT = 'http://localhost:9000';
process.env.PGHOST = 'localhost';
process.env.PGPORT = '5432';
process.env.PGUSER = 'postgres';
process.env.PGPASSWORD = 'password';
process.env.PGDATABASE = 'constructive';
pg = makePg();
await pg.connect();
storage = makeStorage();
await setupFilesStoreSchema(pg);
});
afterEach(async () => {
if (uploadResolverModule) {
await uploadResolverModule.__resetUploadResolverForTests();
uploadResolverModule = null;
}
for (const key of uploadedKeys) {
try {
await storage.delete(key);
} catch {
// ignore cleanup failures for already-deleted objects
}
}
uploadedKeys.clear();
await cleanupFilesStoreRows(pg);
});
afterAll(async () => {
process.env = originalEnv;
await pg.end();
storage.destroy();
});
it('streams a REST upload to storage and inserts a pending files row', async () => {
uploadResolverModule = await loadUploadResolverModule();
const result = await uploadResolverModule.streamToStorage(
Readable.from(MINIMAL_PNG),
'avatar.png',
{
databaseId: '1',
userId: USER_ID,
bucketKey: 'default',
}
);
expect(result.mime).toBe('image/png');
expect(result.filename).toBe('avatar.png');
expect(result.key).toMatch(/^1\/default\/[0-9a-f-]+_origin$/);
uploadedKeys.add(result.key as string);
expect(await objectExists(storage, result.key as string)).toBe(true);
const dbResult = await pg.query(
`SELECT database_id, bucket_key, key, status, created_by, etag
FROM ${SCHEMA}.${TABLE}
WHERE key = $1`,
[result.key]
);
expect(dbResult.rowCount).toBe(1);
expect(dbResult.rows[0]).toEqual(
expect.objectContaining({
database_id: 1,
bucket_key: 'default',
key: result.key,
status: 'pending',
created_by: USER_ID,
})
);
expect(dbResult.rows[0].etag).toEqual(expect.any(String));
expect(dbResult.rows[0].etag.length).toBeGreaterThan(0);
});
it('handles inline image uploads and inserts the same pending files row shape', async () => {
uploadResolverModule = await loadUploadResolverModule();
const imageUploadDefinition = uploadResolverModule.constructiveUploadFieldDefinitions.find(
(definition) => 'name' in definition && definition.name === 'image'
);
if (!imageUploadDefinition) {
throw new Error('Missing image upload definition');
}
const result = await imageUploadDefinition.resolve(
makeUpload('inline.png', MINIMAL_PNG) as any,
{},
{
req: {
api: { databaseId: '1' },
token: { user_id: USER_ID },
},
},
{ uploadPlugin: { tags: {}, type: 'image' } } as any
);
expect(result).toEqual(
expect.objectContaining({
filename: 'inline.png',
mime: 'image/png',
key: expect.stringMatching(/^1\/default\/[0-9a-f-]+_origin$/),
url: expect.any(String),
})
);
const key = (result as { key: string }).key;
uploadedKeys.add(key);
expect(await objectExists(storage, key)).toBe(true);
const dbResult = await pg.query(
`SELECT database_id, bucket_key, key, status, created_by
FROM ${SCHEMA}.${TABLE}
WHERE key = $1`,
[key]
);
expect(dbResult.rowCount).toBe(1);
expect(dbResult.rows[0]).toEqual({
database_id: 1,
bucket_key: 'default',
key,
status: 'pending',
created_by: USER_ID,
});
});
});