|
|
|
@@ -16,7 +16,7 @@ describe('issuing an upload link', () => {
|
|
|
|
|
it('returns the token exactly once, at creation', async () => {
|
|
|
|
|
const created = await request(app)
|
|
|
|
|
.post('/api/admin/upload-links')
|
|
|
|
|
.send({ label: 'Sarah' });
|
|
|
|
|
.send({ label: 'Sarah', email: 'sarah@example.com' });
|
|
|
|
|
|
|
|
|
|
expect(created.status).toBe(201);
|
|
|
|
|
expect(created.body.label).toBe('Sarah');
|
|
|
|
@@ -34,7 +34,7 @@ describe('issuing an upload link', () => {
|
|
|
|
|
it('stores the digest rather than the token', async () => {
|
|
|
|
|
const created = await request(app)
|
|
|
|
|
.post('/api/admin/upload-links')
|
|
|
|
|
.send({ label: 'Estate sale box 3' });
|
|
|
|
|
.send({ label: 'Estate sale box 3', email: 'sarah@example.com' });
|
|
|
|
|
|
|
|
|
|
const { rows } = await pool.query<{ token_hash: string }>(
|
|
|
|
|
`SELECT token_hash FROM upload_links`
|
|
|
|
@@ -51,7 +51,7 @@ describe('issuing an upload link', () => {
|
|
|
|
|
it('refuses a non-positive submission cap', async () => {
|
|
|
|
|
const res = await request(app)
|
|
|
|
|
.post('/api/admin/upload-links')
|
|
|
|
|
.send({ label: 'Bad cap', maxSubmissions: 0 });
|
|
|
|
|
.send({ label: 'Bad cap', email: 'sarah@example.com', maxSubmissions: 0 });
|
|
|
|
|
expect(res.status).toBe(400);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -59,7 +59,9 @@ describe('issuing an upload link', () => {
|
|
|
|
|
// safe. An unbounded link should be something asked for, not something that
|
|
|
|
|
// happens when nobody thought about it.
|
|
|
|
|
it('bounds a link that was created without a cap', async () => {
|
|
|
|
|
const res = await request(app).post('/api/admin/upload-links').send({ label: 'Sarah' });
|
|
|
|
|
const res = await request(app)
|
|
|
|
|
.post('/api/admin/upload-links')
|
|
|
|
|
.send({ label: 'Sarah', email: 'sarah@example.com' });
|
|
|
|
|
|
|
|
|
|
expect(res.status).toBe(201);
|
|
|
|
|
expect(res.body.max_submissions).toBe(25);
|
|
|
|
@@ -68,7 +70,7 @@ describe('issuing an upload link', () => {
|
|
|
|
|
it('allows unlimited when it is asked for explicitly', async () => {
|
|
|
|
|
const res = await request(app)
|
|
|
|
|
.post('/api/admin/upload-links')
|
|
|
|
|
.send({ label: 'Always on', maxSubmissions: null });
|
|
|
|
|
.send({ label: 'Always on', email: 'sarah@example.com', maxSubmissions: null });
|
|
|
|
|
|
|
|
|
|
expect(res.status).toBe(201);
|
|
|
|
|
expect(res.body.max_submissions).toBeNull();
|
|
|
|
@@ -79,7 +81,7 @@ describe('revoking an upload link', () => {
|
|
|
|
|
it('stamps revoked_at and reports it in the listing', async () => {
|
|
|
|
|
const created = await request(app)
|
|
|
|
|
.post('/api/admin/upload-links')
|
|
|
|
|
.send({ label: 'Temporary' });
|
|
|
|
|
.send({ label: 'Temporary', email: 'sarah@example.com' });
|
|
|
|
|
|
|
|
|
|
const revoked = await request(app)
|
|
|
|
|
.post(`/api/admin/upload-links/${created.body.id}/revoke`);
|
|
|
|
@@ -94,7 +96,7 @@ describe('revoking an upload link', () => {
|
|
|
|
|
it('is idempotent, keeping the original timestamp', async () => {
|
|
|
|
|
const created = await request(app)
|
|
|
|
|
.post('/api/admin/upload-links')
|
|
|
|
|
.send({ label: 'Temporary' });
|
|
|
|
|
.send({ label: 'Temporary', email: 'sarah@example.com' });
|
|
|
|
|
|
|
|
|
|
const first = await request(app).post(`/api/admin/upload-links/${created.body.id}/revoke`);
|
|
|
|
|
const second = await request(app).post(`/api/admin/upload-links/${created.body.id}/revoke`);
|
|
|
|
@@ -108,3 +110,99 @@ describe('revoking an upload link', () => {
|
|
|
|
|
expect(res.status).toBe(404);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('emailing the link to its recipient', () => {
|
|
|
|
|
const original = { ...process.env };
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
process.env = { ...original };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('refuses to create a link with no address', async () => {
|
|
|
|
|
const res = await request(app)
|
|
|
|
|
.post('/api/admin/upload-links')
|
|
|
|
|
.send({ label: 'Sarah' });
|
|
|
|
|
|
|
|
|
|
expect(res.status).toBe(400);
|
|
|
|
|
expect(res.body.error).toMatch(/email/i);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('refuses an address that is not one', async () => {
|
|
|
|
|
const res = await request(app)
|
|
|
|
|
.post('/api/admin/upload-links')
|
|
|
|
|
.send({ label: 'Sarah', email: 'not-an-address' });
|
|
|
|
|
|
|
|
|
|
expect(res.status).toBe(400);
|
|
|
|
|
expect(res.body.error).toMatch(/email/i);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('stores the address on the link', async () => {
|
|
|
|
|
const res = await request(app)
|
|
|
|
|
.post('/api/admin/upload-links')
|
|
|
|
|
.send({ label: 'Sarah', email: 'sarah@example.com' });
|
|
|
|
|
|
|
|
|
|
expect(res.status).toBe(201);
|
|
|
|
|
|
|
|
|
|
const { rows } = await pool.query<{ contact_email: string }>(
|
|
|
|
|
`SELECT contact_email FROM upload_links WHERE id = $1`,
|
|
|
|
|
[res.body.id]
|
|
|
|
|
);
|
|
|
|
|
expect(rows[0]?.contact_email).toBe('sarah@example.com');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// The point of the whole change: QA blocks delivery by design, so a link that
|
|
|
|
|
// was not actually emailed must not be reported as though it was.
|
|
|
|
|
it('says the mail was not sent when SMTP is not configured', async () => {
|
|
|
|
|
delete process.env.SMTP_USER;
|
|
|
|
|
delete process.env.SMTP_PASSWORD;
|
|
|
|
|
|
|
|
|
|
const res = await request(app)
|
|
|
|
|
.post('/api/admin/upload-links')
|
|
|
|
|
.send({ label: 'Sarah', email: 'sarah@example.com' });
|
|
|
|
|
|
|
|
|
|
expect(res.status).toBe(201);
|
|
|
|
|
expect(res.body.mail).toEqual({ sent: false, outcome: 'skipped-unconfigured' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('says the mail was not sent when the address is not allowlisted', async () => {
|
|
|
|
|
process.env.SMTP_USER = 'user';
|
|
|
|
|
process.env.SMTP_PASSWORD = 'password';
|
|
|
|
|
process.env.MAIL_ALLOWLIST = 'someone@example.com';
|
|
|
|
|
|
|
|
|
|
const res = await request(app)
|
|
|
|
|
.post('/api/admin/upload-links')
|
|
|
|
|
.send({ label: 'Sarah', email: 'sarah@example.com' });
|
|
|
|
|
|
|
|
|
|
expect(res.status).toBe(201);
|
|
|
|
|
expect(res.body.mail).toEqual({ sent: false, outcome: 'skipped-blocked' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// A send that could not happen must never cost the admin the link, because
|
|
|
|
|
// the token is shown exactly once and a rollback would hand them a different
|
|
|
|
|
// one on the retry.
|
|
|
|
|
it('still returns a usable link when the mail did not go', async () => {
|
|
|
|
|
delete process.env.SMTP_USER;
|
|
|
|
|
|
|
|
|
|
const res = await request(app)
|
|
|
|
|
.post('/api/admin/upload-links')
|
|
|
|
|
.send({ label: 'Sarah', email: 'sarah@example.com' });
|
|
|
|
|
|
|
|
|
|
expect(res.body.token).toBeTruthy();
|
|
|
|
|
expect(res.body.url).toContain(res.body.token);
|
|
|
|
|
expect(res.body.mail.sent).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('lists the address, and tolerates a link that has none', async () => {
|
|
|
|
|
await pool.query(`INSERT INTO upload_links (label, token_hash) VALUES ('Older link', 'digest')`);
|
|
|
|
|
await request(app)
|
|
|
|
|
.post('/api/admin/upload-links')
|
|
|
|
|
.send({ label: 'Newer link', email: 'sarah@example.com' });
|
|
|
|
|
|
|
|
|
|
const res = await request(app).get('/api/admin/upload-links');
|
|
|
|
|
|
|
|
|
|
const older = res.body.find((row: { label: string }) => row.label === 'Older link');
|
|
|
|
|
const newer = res.body.find((row: { label: string }) => row.label === 'Newer link');
|
|
|
|
|
expect(older.contact_email).toBeNull();
|
|
|
|
|
expect(newer.contact_email).toBe('sarah@example.com');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|