Skip to content

Commit a11bda8

Browse files
committed
fix(*): write tests for service and fix revealed bugs
Also: - Add `MemoryRepository` (basically an in-memory db) to support tests - Test the entire Uploads public API surface area - Fix revealed bugs from tests - Actually delete on update (to mimc file move) - Install memfs in order to use a memory disk for testing - Use process.hrtime for generation of filenames to get unique timestamps even in the same tick
1 parent fbf75ea commit a11bda8

File tree

12 files changed

+550
-30
lines changed

12 files changed

+550
-30
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ TODO
2424

2525
- [ ] Usage docs
2626
- [ ] Detailed API docs
27+
- [ ] More tests:
28+
- Test errors when bad `defaultGeneratePath` and/or `defaultSanitizeFilename` are provided.
2729
- [ ] Do URL generation for publicly available disks.
28-
- [ ] Support deleting files was @carimus/node-disks supports it
2930
- [ ] Support temporary URLs (e.g. presigned URLs for S3 buckets) for disks that support it
3031
- [ ] Support transfer logic for transferring single uploads from one disk to another and in bulk.
3132
- [ ] Support `getTemporaryFile` to copy an upload file to the local filesystem tmp directory for direct manipulation
33+
- [ ] Instead of using an `Upload` type that is `any`, we should use generics
3234

3335
## Development
3436

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"husky": "^1.3.1",
5555
"jest": "^24.5.0",
5656
"lint-staged": "^8.1.5",
57+
"memfs": "^2.15.2",
5758
"npm-watch": "^0.6.0",
5859
"prettier": "1.16.4",
5960
"semantic-release": "^15.13.3",

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from './types';
55
export * from './lib/Uploads';
66
export * from './lib/defaults';
77
export * from './errors';
8+
export * from './support'

src/lib/Uploads.test.ts

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import { DiskDriver, DiskManager } from '@carimus/node-disks';
2+
import { MemoryRepository } from '../support';
3+
import { Uploads } from './Uploads';
4+
import { UploadMeta } from '../types';
5+
6+
const disks = {
7+
default: 'memory',
8+
memory: {
9+
driver: DiskDriver.Memory,
10+
},
11+
};
12+
13+
function setup(): {
14+
diskManager: DiskManager;
15+
repository: MemoryRepository;
16+
uploads: Uploads;
17+
} {
18+
const diskManager = new DiskManager(disks);
19+
const repository = new MemoryRepository();
20+
return {
21+
diskManager,
22+
repository,
23+
uploads: new Uploads({
24+
disks: diskManager,
25+
repository,
26+
}),
27+
};
28+
}
29+
30+
const files: {
31+
[key: string]: { uploadedAs: string; data: Buffer; meta: UploadMeta };
32+
} = {
33+
normal: {
34+
uploadedAs: 'foo.txt',
35+
data: Buffer.from('This is a normal text test file.\n', 'utf8'),
36+
meta: { context: 'test', isFoo: true },
37+
},
38+
image: {
39+
uploadedAs: 'black-1x1.png',
40+
data: Buffer.from(
41+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
42+
'base64',
43+
),
44+
meta: { context: 'test', isFoo: false, isImage: true },
45+
},
46+
weirdName: {
47+
uploadedAs: '.~my~cool~data~&^%$*(¶•ª•.csv',
48+
data: Buffer.from('a,b,c\nfoo,bar,baz\n1,2,3\n', 'utf8'),
49+
meta: { context: 'test', isFoo: false, isImage: true },
50+
},
51+
};
52+
53+
test('Uploads service can upload and persist to repository.', async () => {
54+
const { diskManager, repository, uploads } = setup();
55+
56+
// Upload all the files
57+
const normalUpload = await uploads.upload(
58+
files.normal.data,
59+
files.normal.uploadedAs,
60+
files.normal.meta,
61+
);
62+
const imageUpload = await uploads.upload(
63+
files.image.data,
64+
files.image.uploadedAs,
65+
files.image.meta,
66+
);
67+
const weirdNameUpload = await uploads.upload(
68+
files.weirdName.data,
69+
files.weirdName.uploadedAs,
70+
files.weirdName.meta,
71+
);
72+
73+
// Check the repository for the file data
74+
const normalFileInfo = await repository.getUploadedFileInfo(normalUpload);
75+
const imageFileInfo = await repository.getUploadedFileInfo(imageUpload);
76+
const weirdNameFileInfo = await repository.getUploadedFileInfo(
77+
weirdNameUpload,
78+
);
79+
expect(normalFileInfo).toBeTruthy();
80+
expect(typeof normalFileInfo).toBe('object');
81+
expect(imageFileInfo).toBeTruthy();
82+
expect(typeof imageFileInfo).toBe('object');
83+
expect(weirdNameFileInfo).toBeTruthy();
84+
expect(typeof weirdNameFileInfo).toBe('object');
85+
86+
// Check the disk for disk data
87+
const normalDiskData = await diskManager
88+
.getDisk(normalFileInfo.disk)
89+
.read(normalFileInfo.path);
90+
const imageDiskData = await diskManager
91+
.getDisk(imageFileInfo.disk)
92+
.read(imageFileInfo.path);
93+
const weirdNameDiskData = await diskManager
94+
.getDisk(weirdNameFileInfo.disk)
95+
.read(weirdNameFileInfo.path);
96+
expect(normalDiskData.toString('base64')).toBe(
97+
files.normal.data.toString('base64'),
98+
);
99+
expect(imageDiskData.toString('base64')).toBe(
100+
files.image.data.toString('base64'),
101+
);
102+
expect(weirdNameDiskData.toString('base64')).toBe(
103+
files.weirdName.data.toString('base64'),
104+
);
105+
});
106+
107+
test('Uploads service can duplicate an upload, creating a new repository record for it.', async () => {
108+
const { diskManager, repository, uploads } = setup();
109+
110+
// Upload a file and then duplicate it with new metadata (and also with no new meta)
111+
const original = await uploads.upload(
112+
files.normal.data,
113+
files.normal.uploadedAs,
114+
files.normal.meta,
115+
);
116+
const duplicateMeta = { ...files.normal.meta, isDup: true };
117+
const duplicate = await uploads.duplicate(original, duplicateMeta);
118+
const duplicateOldMeta = await uploads.duplicate(original);
119+
120+
// Check the repository for the original file data and the new file data
121+
const originalFileRecord = await repository.find(original.id);
122+
const duplicateFileRecord = await repository.find(duplicate.id);
123+
const duplicateOldMetaFileRecord = await repository.find(
124+
duplicateOldMeta.id,
125+
);
126+
expect(originalFileRecord.meta).toMatchObject(files.normal.meta);
127+
expect(duplicateFileRecord.meta).toMatchObject(duplicateMeta);
128+
expect(originalFileRecord).not.toMatchObject(duplicateFileRecord);
129+
expect(duplicateOldMetaFileRecord.meta).toMatchObject(files.normal.meta);
130+
131+
// Check the disk for the original file and the new file
132+
const originalFileInfo = await repository.getUploadedFileInfo(original.id);
133+
const duplicateFileInfo = await repository.getUploadedFileInfo(
134+
duplicate.id,
135+
);
136+
const originalFileData = await diskManager
137+
.getDisk(originalFileInfo.disk)
138+
.read(originalFileInfo.path);
139+
const duplicateFileData = await diskManager
140+
.getDisk(duplicateFileInfo.disk)
141+
.read(duplicateFileInfo.path);
142+
expect(originalFileData.toString('base64')).toBe(
143+
files.normal.data.toString('base64'),
144+
);
145+
expect(duplicateFileData.toString('base64')).toBe(
146+
files.normal.data.toString('base64'),
147+
);
148+
});
149+
150+
test('Uploads service can update, updating existing repository records and deleting old files.', async () => {
151+
const { diskManager, repository, uploads } = setup();
152+
153+
// Upload a file and check to ensure it was created on the disk
154+
const original = await uploads.upload(
155+
files.normal.data,
156+
files.normal.uploadedAs,
157+
files.normal.meta,
158+
);
159+
const originalFileInfo = await repository.getUploadedFileInfo(original);
160+
await expect(
161+
diskManager.getDisk(originalFileInfo.disk).read(originalFileInfo.path),
162+
).resolves.toBeTruthy();
163+
164+
// Update the file, get its new info, ensure the new file exists and that the old one was deleted.
165+
const updatedNewMeta = await uploads.update(
166+
original,
167+
files.image.data,
168+
files.image.uploadedAs,
169+
files.image.meta,
170+
);
171+
const updatedNewMetaFileInfo = await repository.getUploadedFileInfo(
172+
updatedNewMeta,
173+
);
174+
expect(original.id).toBe(updatedNewMeta.id);
175+
await expect(
176+
diskManager.getDisk(originalFileInfo.disk).read(originalFileInfo.path),
177+
).rejects.toBeTruthy();
178+
await expect(
179+
diskManager
180+
.getDisk(updatedNewMetaFileInfo.disk)
181+
.read(updatedNewMetaFileInfo.path),
182+
).resolves.toBeTruthy();
183+
expect(await repository.getMeta(updatedNewMeta)).toMatchObject(
184+
files.image.meta,
185+
);
186+
187+
// Let's also ensure that meta is preserved if we don't pass any new meta
188+
const updatedOldMeta = await uploads.update(
189+
updatedNewMeta,
190+
files.weirdName.data,
191+
files.weirdName.uploadedAs,
192+
);
193+
const updatedOldMetaFileInfo = await repository.getUploadedFileInfo(
194+
updatedOldMeta,
195+
);
196+
await expect(
197+
diskManager.getDisk(originalFileInfo.disk).read(originalFileInfo.path),
198+
).rejects.toBeTruthy();
199+
await expect(
200+
diskManager
201+
.getDisk(updatedNewMetaFileInfo.disk)
202+
.read(updatedNewMetaFileInfo.path),
203+
).rejects.toBeTruthy();
204+
await expect(
205+
diskManager
206+
.getDisk(updatedOldMetaFileInfo.disk)
207+
.read(updatedOldMetaFileInfo.path),
208+
).resolves.toBeTruthy();
209+
expect(await repository.getMeta(updatedOldMeta)).toMatchObject(
210+
files.image.meta,
211+
);
212+
});
213+
214+
test('Uploads service can delete', async () => {
215+
const { diskManager, repository, uploads } = setup();
216+
217+
// Upload a file and check to ensure it was created on the disk
218+
const upload = await uploads.upload(
219+
files.normal.data,
220+
files.normal.uploadedAs,
221+
files.normal.meta,
222+
);
223+
const fileInfo = await repository.getUploadedFileInfo(upload);
224+
await expect(repository.getMeta(upload)).resolves.toBeTruthy();
225+
await expect(
226+
diskManager.getDisk(fileInfo.disk).read(fileInfo.path),
227+
).resolves.toBeTruthy();
228+
229+
// Delete the upload and ensure it was deleted both from the disk and the repository.
230+
await uploads.delete(upload);
231+
await expect(repository.getUploadedFileInfo(upload)).rejects.toBeTruthy();
232+
await expect(repository.getMeta(upload)).rejects.toBeTruthy();
233+
await expect(
234+
diskManager.getDisk(fileInfo.disk).read(fileInfo.path),
235+
).rejects.toBeTruthy();
236+
});
237+
238+
test('Uploads service can delete only the file', async () => {
239+
const { diskManager, repository, uploads } = setup();
240+
241+
// Upload a file and check to ensure it was created on the disk
242+
const upload = await uploads.upload(
243+
files.normal.data,
244+
files.normal.uploadedAs,
245+
files.normal.meta,
246+
);
247+
const fileInfo = await repository.getUploadedFileInfo(upload);
248+
await expect(repository.getMeta(upload)).resolves.toBeTruthy();
249+
await expect(
250+
diskManager.getDisk(fileInfo.disk).read(fileInfo.path),
251+
).resolves.toBeTruthy();
252+
253+
// Delete the upload and ensure it was deleted from the disk but **NOT** the repository
254+
await uploads.delete(upload, true);
255+
await expect(repository.getUploadedFileInfo(upload)).resolves.toBeTruthy();
256+
await expect(repository.getMeta(upload)).resolves.toBeTruthy();
257+
await expect(
258+
diskManager.getDisk(fileInfo.disk).read(fileInfo.path),
259+
).rejects.toBeTruthy();
260+
});

0 commit comments

Comments
 (0)