Skip to content

Commit eaff96f

Browse files
authored
Start adding integration tests on app (#49)
* setup jest * split app creation and app launch * Add tests for file and history endpoints * fix package-lock.json * update package-lock * add sinon as an explicit dependency
1 parent 669222a commit eaff96f

File tree

5 files changed

+265
-211
lines changed

5 files changed

+265
-211
lines changed

app.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import supertest from 'supertest';
2+
import { ImportMock } from 'ts-mock-imports';
3+
4+
import { pprint } from './src/helpers';
5+
import * as Ledger from './src/ledger';
6+
7+
import app from './app';
8+
9+
// TODO
10+
// - isolate test env (pass outDir as a param to app, create a specific outDir
11+
// for test)
12+
// - test that an existing file is returned
13+
// - test that an existing dotFile is not returned
14+
describe('The file endpoint', () => {
15+
it('should 404 if the file doesnt exist.', () => {
16+
return supertest(app)
17+
.get('/file/foo')
18+
.then(response => {
19+
expect(response.status).toBe(404);
20+
});
21+
});
22+
23+
it('should 200 with the file if it exists', () => {
24+
// create a file in the test dir
25+
// ask for it
26+
// file is returned
27+
});
28+
29+
it('should not return an existing dot file', () => {
30+
// create a dot file in the test dir
31+
// ask for it
32+
// file is not returned
33+
});
34+
});
35+
36+
describe('The history endpoint', () => {
37+
afterEach(() => {
38+
ImportMock.restore();
39+
});
40+
41+
it("should use the ledger's history API", () => {
42+
const result = { foo: 'bar' };
43+
44+
const getListHistoryMock = ImportMock.mockFunction(
45+
Ledger,
46+
'listDocHistory',
47+
Promise.resolve(result)
48+
);
49+
50+
const name = 'foo';
51+
52+
return supertest(app)
53+
.get(`/history/${name}`)
54+
.then(response => {
55+
expect(getListHistoryMock.calledOnce).toBe(true);
56+
expect(getListHistoryMock.calledWith(name)).toBe(true);
57+
58+
expect(response.status).toBe(200);
59+
expect(response.text).toBe(pprint(result));
60+
});
61+
});
62+
});
63+
64+
describe('The export-copy endpoint', () => {});

app.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import express, { Application, Request, Response } from 'express';
2+
import { config } from 'dotenv';
3+
import { join } from 'path';
4+
import cors from 'cors';
5+
import sharp from 'sharp';
6+
import formidable, { Fields, Files } from 'formidable';
7+
import chalk from 'chalk';
8+
import fs from 'fs/promises';
9+
import { parse } from 'path';
10+
11+
import * as Ledger from './src/ledger';
12+
import * as Store from './src/store';
13+
import * as Bundle from './src/types/Bundle';
14+
import * as Record from './src/types/Record';
15+
import * as Verify from './src/verify';
16+
17+
import { pprint, cleanupBase64 } from './src/helpers';
18+
19+
// set up .env variables as environment variables
20+
config();
21+
22+
const app: Application = express();
23+
24+
app.use(cors());
25+
26+
// Body parsing Middleware
27+
app.use(express.json());
28+
app.use(express.urlencoded({ extended: true }));
29+
30+
app.get('/file/:sku', async (req: Request, res: Response): Promise<void> => {
31+
const { sku } = req.params;
32+
const options = {
33+
root: join(__dirname, './out'),
34+
dotfiles: 'deny',
35+
};
36+
res.sendFile(`${sku}`, options);
37+
});
38+
39+
app.get(
40+
'/history/:sku',
41+
async (req: Request, res: Response): Promise<Response> => {
42+
const { sku } = req.params;
43+
const result = await Ledger.listDocHistory(sku);
44+
console.log(chalk.bold(`GET /history/${sku}`));
45+
return res.status(200).send(pprint(result));
46+
}
47+
);
48+
49+
app.get(
50+
'/export-copy/:sku',
51+
async (req: Request, res: Response): Promise<void> => {
52+
const { sku } = req.params;
53+
const rootDir = join(__dirname, './out');
54+
const options = {
55+
root: rootDir,
56+
dotfiles: 'deny',
57+
};
58+
// `sku` comes with .zip at the end, which we must remove
59+
const cleanSku = parse(sku).name;
60+
await Ledger.getDoc(cleanSku, 'id')
61+
.then(r => Store.makeZip(r, rootDir, rootDir))
62+
.then(() =>
63+
res
64+
.set(`Content-Type`, `application/octet-stream`)
65+
.set(`Content-Disposition`, `attachment; filename=${sku}`)
66+
.sendFile(`${sku}`, options)
67+
);
68+
}
69+
);
70+
71+
app.get(
72+
'/list-docs',
73+
async (req: Request, res: Response): Promise<Response> => {
74+
const result = await Ledger.listDocs();
75+
console.log(chalk.bold(`GET /list-docs`));
76+
return res.status(200).send(pprint(result));
77+
}
78+
);
79+
80+
app.post('/form', async (req: Request, res: Response): Promise<Response> => {
81+
console.log(chalk.bold(`POST /form`));
82+
return new Promise((resolve, reject) => {
83+
const form = new formidable.IncomingForm();
84+
form.parse(req, async (err: Error, fields: Fields): Promise<void> => {
85+
if (err) {
86+
resolve(res.status(400).send(`${err.name}: ${err.message}`)); // FIXME: don't expose js errors to public
87+
}
88+
const { url, title, scr } = fields;
89+
const base64Data = cleanupBase64(scr as string); // FIXME: don't use 'as string', instead make sure we have string instead of string[]
90+
const screenshotData = new Buffer(base64Data, 'base64');
91+
const thumbnailData = await sharp(screenshotData)
92+
.resize(320, 240, { fit: 'inside' })
93+
.toBuffer();
94+
let onefileData: string;
95+
onefileData = fields.onefile as string;
96+
97+
const screenshot = { kind: 'screenshot' as const, data: screenshotData };
98+
const thumbnail = {
99+
kind: 'screenshot_thumbnail' as const,
100+
data: thumbnailData,
101+
};
102+
const onefile = { kind: 'one_file' as const, data: onefileData };
103+
104+
await Store.newBundle([screenshot, thumbnail, onefile])
105+
.then((bundle: Bundle.Bundle) => {
106+
const record = {
107+
bundle,
108+
annotations: { description: '' },
109+
data: { url: url as string, title: title as string },
110+
};
111+
return record;
112+
})
113+
.then((r: Record.Record) => {
114+
return Ledger.insertDoc(r);
115+
})
116+
.then(_ => {
117+
console.log(`ledger inserted correctly`);
118+
resolve(res.status(200).send('Received POST on /form'));
119+
})
120+
.catch(e => {
121+
resolve(
122+
res.status(422).send(`${e.name} (type ${e.type}): ${e.message}`)
123+
);
124+
});
125+
});
126+
});
127+
});
128+
129+
app.post(
130+
'/edit-description/:sku',
131+
async (req: Request, res: Response): Promise<Response> => {
132+
const { sku } = req.params;
133+
console.log(chalk.bold(`POST /edit-description/${sku}`));
134+
return new Promise((resolve, reject) => {
135+
const form = new formidable.IncomingForm();
136+
form.parse(req, async (err: Error, fields: Fields): Promise<void> => {
137+
if (err) {
138+
resolve(res.status(400).send(`${err.name}: ${err.message}`)); // FIXME: don't expose js errors to public
139+
}
140+
const { description } = fields;
141+
const update = { description: description as string };
142+
Ledger.updateDoc(sku, update);
143+
144+
resolve(res.status(200).send(`Wrote description to ${sku}`));
145+
});
146+
});
147+
}
148+
);
149+
150+
app.post('/verify', async (req: Request, res: Response): Promise<Response> => {
151+
console.log(chalk.bold(`POST /verify`));
152+
return new Promise((resolve, reject) => {
153+
const form = new formidable.IncomingForm();
154+
form.parse(req, async (err: Error, fields: Fields, files: Files) => {
155+
// verify each file by opening it and sending it to check
156+
// (which involves hashing and comparing to QLDB), then building
157+
// the Response one file at a time and sending it once
158+
Promise.all(
159+
Object.keys(files).map(async i => {
160+
await fs
161+
// @ts-expect-error
162+
.readFile(files[i].path)
163+
.then(f => Verify.verifyFile(f))
164+
.then(result =>
165+
res.write(
166+
result
167+
? JSON.stringify(result)
168+
: JSON.stringify({ not_found: 'item not found' })
169+
)
170+
);
171+
})
172+
).then(() => resolve(res.send()));
173+
});
174+
});
175+
});
176+
177+
export default app;

0 commit comments

Comments
 (0)