All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m15s
CI / Build container images (pull_request) Successful in 4m27s
CI / Auth e2e pack (pull_request) Successful in 9m10s
CI / Import/export fidelity gate (pull_request) Successful in 53s
CD / Build and push images (push) Successful in 17s
CD / Deploy to Test (push) Successful in 15s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 20s
CI / Lint, typecheck, test (push) Successful in 5m47s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m26s
CI / Import/export fidelity gate (push) Successful in 1m0s
The attachments panel of a classified page shows a persistent notice naming the consequence (de+en): the file inherits the page's classification but its content carries no marking (#212). The new instance setting classification.uploadPolicy (default warn, documented; the VS-NfD reference configuration blocks, #227) hardens the warning into a server-side rejection (403 classified_upload_blocked) — enforced in the upload service, not only in the UI. Tests: warning visible in the local attachments pack; block enforced server-side with warn/block both ways and open pages unaffected. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
499 lines
19 KiB
TypeScript
499 lines
19 KiB
TypeScript
import { existsSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
import { INestApplication } from '@nestjs/common';
|
|
import { PrismaClient } from '@prisma/client';
|
|
import request from 'supertest';
|
|
import type { Test } from 'supertest';
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
|
|
import { AuthTokensService } from '../auth/auth-tokens.service';
|
|
import { InstanceSettingsService } from '../settings/instance-settings.service';
|
|
import { createTestApp, sessionCookieOf } from '../testing/test-app';
|
|
import { createTestPrisma, hasTestDb, uniqueSuffix } from '../testing/test-db';
|
|
import { UsersService } from '../users/users.service';
|
|
|
|
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
const pngBuffer = (payload = 'fake-but-signed png bytes'): Buffer =>
|
|
Buffer.concat([PNG_SIGNATURE, Buffer.from(payload)]);
|
|
|
|
/** superagent has no default parser for image/*; buffer the raw bytes ourselves. */
|
|
function binaryParser(
|
|
res: NodeJS.ReadableStream,
|
|
callback: (err: Error | null, body: Buffer) => void,
|
|
): void {
|
|
const chunks: Buffer[] = [];
|
|
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
res.on('end', () => callback(null, Buffer.concat(chunks)));
|
|
}
|
|
type ParseCallback = Parameters<Test['parse']>[0];
|
|
|
|
describe.skipIf(!hasTestDb)('files (e2e, issue #27)', () => {
|
|
let app: INestApplication;
|
|
let prisma: PrismaClient;
|
|
const suffix = uniqueSuffix();
|
|
const password = 'bilder hochladen ist toll 1';
|
|
|
|
const owner = { username: `fiona-files-${suffix}`, displayName: `Fiona Files ${suffix}` };
|
|
const outsider = { username: `otto-files-${suffix}`, displayName: `Otto Outside ${suffix}` };
|
|
let ownerCookie: string;
|
|
let outsiderCookie: string;
|
|
let pondId: string;
|
|
|
|
const api = () => request(app.getHttpServer());
|
|
|
|
async function loginOf(username: string): Promise<string> {
|
|
const res = await api()
|
|
.post('/api/v1/auth/login')
|
|
.send({ usernameOrEmail: username, password })
|
|
.expect(200);
|
|
return sessionCookieOf(res);
|
|
}
|
|
|
|
async function setPondOverride(quotaKey: string, value: number): Promise<void> {
|
|
await prisma.quotaOverride.upsert({
|
|
where: {
|
|
subjectType_subjectId_quotaKey: { subjectType: 'POND', subjectId: pondId, quotaKey },
|
|
},
|
|
create: { subjectType: 'POND', subjectId: pondId, quotaKey, value },
|
|
update: { value },
|
|
});
|
|
}
|
|
|
|
async function clearPondOverrides(): Promise<void> {
|
|
await prisma.quotaOverride.deleteMany({ where: { subjectType: 'POND', subjectId: pondId } });
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
prisma = createTestPrisma();
|
|
await prisma.rateLimit.deleteMany({});
|
|
app = await createTestApp();
|
|
const users = app.get(UsersService);
|
|
const tokens = app.get(AuthTokensService);
|
|
|
|
const ownerUser = await users.createUser({
|
|
username: owner.username,
|
|
email: `${owner.username}@example.org`,
|
|
displayName: owner.displayName,
|
|
password,
|
|
locale: 'en',
|
|
});
|
|
const verifyToken = await tokens.issue(ownerUser.id, 'EMAIL_VERIFICATION', 600);
|
|
await api().post('/api/v1/auth/verify-email').send({ token: verifyToken }).expect(204);
|
|
ownerCookie = await loginOf(owner.username);
|
|
|
|
const outsiderUser = await users.createUser({
|
|
username: outsider.username,
|
|
email: `${outsider.username}@example.org`,
|
|
displayName: outsider.displayName,
|
|
password,
|
|
locale: 'en',
|
|
});
|
|
await users.markEmailVerified(outsiderUser.id);
|
|
outsiderCookie = await loginOf(outsider.username);
|
|
|
|
const ponds = await api().get('/api/v1/ponds').set('Cookie', ownerCookie).expect(200);
|
|
pondId = ponds.body.find((p: { type: string }) => p.type === 'personal').id;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await prisma.attachment.deleteMany({ where: { pondId } });
|
|
await prisma.page.deleteMany({
|
|
where: { pond: { owner: { username: { contains: suffix } } } },
|
|
});
|
|
const users = await prisma.user.findMany({
|
|
where: { username: { contains: suffix } },
|
|
select: { id: true },
|
|
});
|
|
await prisma.quotaOverride.deleteMany({
|
|
where: { subjectId: { in: [pondId, ...users.map((u) => u.id)] } },
|
|
});
|
|
await prisma.pond.deleteMany({ where: { owner: { username: { contains: suffix } } } });
|
|
await prisma.user.deleteMany({ where: { username: { contains: suffix } } });
|
|
await prisma.$disconnect();
|
|
await app.close();
|
|
});
|
|
|
|
it('roundtrips an uploaded image: same bytes, sniffed content type', async () => {
|
|
const bytes = pngBuffer('roundtrip');
|
|
const uploaded = await api()
|
|
.post(`/api/v1/ponds/${pondId}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', bytes, 'photo.png')
|
|
.expect(201);
|
|
expect(uploaded.body.mimeType).toBe('image/png');
|
|
expect(uploaded.body.sizeBytes).toBe(bytes.length);
|
|
|
|
const served = await api()
|
|
.get(`/api/v1/media/${uploaded.body.id}`)
|
|
.set('Cookie', ownerCookie)
|
|
.buffer(true)
|
|
.parse(binaryParser as unknown as ParseCallback)
|
|
.expect(200);
|
|
expect(served.headers['content-type']).toBe('image/png');
|
|
expect(served.headers['x-content-type-options']).toBe('nosniff');
|
|
expect(Buffer.compare(served.body, bytes)).toBe(0);
|
|
});
|
|
|
|
it('accepts an allowlisted non-image file and serves it as a download (#61)', async () => {
|
|
const pdf = Buffer.from('%PDF-1.4 minimal but allowlisted');
|
|
const uploaded = await api()
|
|
.post(`/api/v1/ponds/${pondId}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', pdf, 'document.pdf')
|
|
.expect(201);
|
|
expect(uploaded.body.mimeType).toBe('application/pdf');
|
|
|
|
const served = await api()
|
|
.get(`/api/v1/media/${uploaded.body.id}`)
|
|
.set('Cookie', ownerCookie)
|
|
.buffer(true)
|
|
.parse(binaryParser as unknown as ParseCallback)
|
|
.expect(200);
|
|
// Non-images are always downloads, never inline (ADR 0011, security.md).
|
|
expect(served.headers['content-disposition']).toContain('attachment');
|
|
expect(served.headers['content-disposition']).toContain('document.pdf');
|
|
expect(served.headers['content-type']).toContain('application/pdf');
|
|
});
|
|
|
|
it('rejects a file whose extension is not on the allowlist (#61)', async () => {
|
|
const res = await api()
|
|
.post(`/api/v1/ponds/${pondId}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', Buffer.from('binary junk'), 'malware.exe')
|
|
.expect(400);
|
|
expect(res.body.code).toBe('upload_type_not_allowed');
|
|
expect(res.body.details.allowed).toContain('pdf');
|
|
});
|
|
|
|
it('rejects a renamed .html-as-.png via the magic-byte check (#61)', async () => {
|
|
const res = await api()
|
|
.post(`/api/v1/ponds/${pondId}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', Buffer.from('<html><body>evil</body></html>'), 'sneaky.png')
|
|
.expect(400);
|
|
// png is validated by bytes, not extension, so the disguise fails the
|
|
// allowlist (png is not a configured non-image extension either).
|
|
expect(res.body.code).toBe('upload_type_not_allowed');
|
|
});
|
|
|
|
it('sanitizes an uploaded SVG, stripping scripts and event handlers (#61)', async () => {
|
|
const settings = app.get(InstanceSettingsService);
|
|
await settings.set('upload.svgPolicy', 'sanitize', 'test');
|
|
const dirty = Buffer.from(
|
|
'<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)">' +
|
|
'<script>alert(2)</script><rect width="10" height="10"/></svg>',
|
|
);
|
|
const uploaded = await api()
|
|
.post(`/api/v1/ponds/${pondId}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', dirty, 'diagram.svg')
|
|
.expect(201);
|
|
expect(uploaded.body.mimeType).toBe('image/svg+xml');
|
|
|
|
const served = await api()
|
|
.get(`/api/v1/media/${uploaded.body.id}`)
|
|
.set('Cookie', ownerCookie)
|
|
.buffer(true)
|
|
.parse(binaryParser as unknown as ParseCallback)
|
|
.expect(200);
|
|
const cleaned = served.body.toString('utf8').toLowerCase();
|
|
expect(cleaned).not.toContain('<script');
|
|
expect(cleaned).not.toContain('onload');
|
|
expect(cleaned).toContain('<rect');
|
|
// SVG is always a download, never inline.
|
|
expect(served.headers['content-disposition']).toContain('attachment');
|
|
});
|
|
|
|
it('rejects an SVG upload when the policy is reject (#61)', async () => {
|
|
const settings = app.get(InstanceSettingsService);
|
|
await settings.set('upload.svgPolicy', 'reject', 'test');
|
|
try {
|
|
const res = await api()
|
|
.post(`/api/v1/ponds/${pondId}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', Buffer.from('<svg xmlns="http://www.w3.org/2000/svg"/>'), 'x.svg')
|
|
.expect(400);
|
|
expect(res.body.code).toBe('upload_type_not_allowed');
|
|
} finally {
|
|
await settings.set('upload.svgPolicy', 'sanitize', 'test');
|
|
}
|
|
});
|
|
|
|
it('rejects an oversize file with a distinct error from quota_exceeded', async () => {
|
|
await setPondOverride('max_file_bytes', 10);
|
|
try {
|
|
const res = await api()
|
|
.post(`/api/v1/ponds/${pondId}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', pngBuffer('this payload is well beyond ten bytes'), 'big.png')
|
|
.expect(413);
|
|
expect(res.body.code).toBe('file_too_large');
|
|
expect(res.body.details.limitBytes).toBe(10);
|
|
} finally {
|
|
await clearPondOverrides();
|
|
}
|
|
});
|
|
|
|
it('rejects uploads that exceed the storage quota', async () => {
|
|
await setPondOverride('max_file_bytes', 10_000);
|
|
await setPondOverride('storage_bytes', 5);
|
|
try {
|
|
const res = await api()
|
|
.post(`/api/v1/ponds/${pondId}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', pngBuffer('needs more than five bytes of budget'), 'photo.png')
|
|
.expect(403);
|
|
expect(res.body.code).toBe('quota_exceeded');
|
|
expect(res.body.details.quotaKey).toBe('storage_bytes');
|
|
} finally {
|
|
await clearPondOverrides();
|
|
}
|
|
});
|
|
|
|
it('releases quota and removes the on-disk bytes on delete', async () => {
|
|
const before = await prisma.pondUsage.findUnique({ where: { pondId } });
|
|
const startUsage = Number(before?.storageBytesUsed ?? 0);
|
|
|
|
const bytes = pngBuffer('to be deleted');
|
|
const uploaded = await api()
|
|
.post(`/api/v1/ponds/${pondId}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', bytes, 'delete-me.png')
|
|
.expect(201);
|
|
|
|
const afterUpload = await prisma.pondUsage.findUniqueOrThrow({ where: { pondId } });
|
|
expect(Number(afterUpload.storageBytesUsed)).toBe(startUsage + bytes.length);
|
|
|
|
const filePath = join(process.env.UPLOADS_DIR!, pondId, uploaded.body.id);
|
|
expect(existsSync(filePath)).toBe(true);
|
|
|
|
await api().delete(`/api/v1/files/${uploaded.body.id}`).set('Cookie', ownerCookie).expect(204);
|
|
|
|
expect(existsSync(filePath)).toBe(false);
|
|
const afterDelete = await prisma.pondUsage.findUniqueOrThrow({ where: { pondId } });
|
|
expect(Number(afterDelete.storageBytesUsed)).toBe(startUsage);
|
|
await api().get(`/api/v1/media/${uploaded.body.id}`).set('Cookie', ownerCookie).expect(404);
|
|
});
|
|
|
|
it('keeps a file reachable after its page is soft-deleted (no eager purge)', async () => {
|
|
const page = await api()
|
|
.post(`/api/v1/ponds/${pondId}/pages`)
|
|
.set('Cookie', ownerCookie)
|
|
.send({ title: `With Attachment ${suffix}` })
|
|
.expect(201);
|
|
|
|
const uploaded = await api()
|
|
.post(`/api/v1/ponds/${pondId}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', pngBuffer('page-attached'), 'inline.png')
|
|
.expect(201);
|
|
// #27 does not expose pageId on the upload endpoint yet (it is set once
|
|
// a future story tracks a page's embedded attachments) — associate it
|
|
// directly to exercise the "stays until purge" guarantee now.
|
|
await prisma.attachment.update({
|
|
where: { id: uploaded.body.id },
|
|
data: { pageId: page.body.id },
|
|
});
|
|
|
|
await api().delete(`/api/v1/pages/${page.body.id}`).set('Cookie', ownerCookie).expect(204);
|
|
|
|
await api().get(`/api/v1/media/${uploaded.body.id}`).set('Cookie', ownerCookie).expect(200);
|
|
const stillThere = await prisma.attachment.findUnique({ where: { id: uploaded.body.id } });
|
|
expect(stillThere).not.toBeNull();
|
|
});
|
|
|
|
it('lists a page attachment for the page and links it (#61)', async () => {
|
|
const page = await api()
|
|
.post(`/api/v1/ponds/${pondId}/pages`)
|
|
.set('Cookie', ownerCookie)
|
|
.send({ title: `Page Files ${suffix}` })
|
|
.expect(201);
|
|
|
|
const uploaded = await api()
|
|
.post(`/api/v1/pages/${page.body.id}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', Buffer.from('%PDF-1.4 attached to a page'), 'report.pdf')
|
|
.expect(201);
|
|
expect(uploaded.body.pageId).toBe(page.body.id);
|
|
|
|
const listed = await api()
|
|
.get(`/api/v1/pages/${page.body.id}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.expect(200);
|
|
const item = listed.body.find((f: { id: string }) => f.id === uploaded.body.id);
|
|
expect(item).toBeDefined();
|
|
expect(item.uploaderName).toBe(owner.displayName);
|
|
expect(item.pageTitle).toBe(`Page Files ${suffix}`);
|
|
});
|
|
|
|
it('prefixes downloads of classified attachments; unset pageId fails closed (issue #212)', async () => {
|
|
const page = await api()
|
|
.post(`/api/v1/ponds/${pondId}/pages`)
|
|
.set('Cookie', ownerCookie)
|
|
.send({ title: `Classified Files ${suffix}` })
|
|
.expect(201);
|
|
|
|
const uploaded = await api()
|
|
.post(`/api/v1/pages/${page.body.id}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', Buffer.from('%PDF-1.4 classified content'), 'geheim.pdf')
|
|
.expect(201);
|
|
|
|
// Unclassified page: unchanged filename.
|
|
const openServed = await api()
|
|
.get(`/api/v1/media/${uploaded.body.id}`)
|
|
.set('Cookie', ownerCookie)
|
|
.buffer(true)
|
|
.parse(binaryParser as unknown as ParseCallback)
|
|
.expect(200);
|
|
expect(openServed.headers['content-disposition']).toContain('filename="geheim.pdf"');
|
|
|
|
// Classified page: the documented VS-NfD_ prefix.
|
|
await prisma.page.update({
|
|
where: { id: page.body.id as string },
|
|
data: { classification: 'VS_NFD' },
|
|
});
|
|
const served = await api()
|
|
.get(`/api/v1/media/${uploaded.body.id}`)
|
|
.set('Cookie', ownerCookie)
|
|
.buffer(true)
|
|
.parse(binaryParser as unknown as ParseCallback)
|
|
.expect(200);
|
|
expect(served.headers['content-disposition']).toContain('filename="VS-NfD_geheim.pdf"');
|
|
|
|
// pageId unset (paste-then-insert): fails closed to the pond's highest
|
|
// level — the pond now contains a classified page, so the orphan upload
|
|
// is served with the prefix too.
|
|
const orphan = await api()
|
|
.post(`/api/v1/ponds/${pondId}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', Buffer.from('%PDF-1.4 orphan bytes'), 'lose-datei.pdf')
|
|
.expect(201);
|
|
const orphanServed = await api()
|
|
.get(`/api/v1/media/${orphan.body.id}`)
|
|
.set('Cookie', ownerCookie)
|
|
.buffer(true)
|
|
.parse(binaryParser as unknown as ParseCallback)
|
|
.expect(200);
|
|
expect(orphanServed.headers['content-disposition']).toContain(
|
|
'filename="VS-NfD_lose-datei.pdf"',
|
|
);
|
|
|
|
// Back to all-open: the orphan serves unprefixed again.
|
|
await prisma.page.update({
|
|
where: { id: page.body.id as string },
|
|
data: { classification: 'UNCLASSIFIED' },
|
|
});
|
|
const openOrphan = await api()
|
|
.get(`/api/v1/media/${orphan.body.id}`)
|
|
.set('Cookie', ownerCookie)
|
|
.buffer(true)
|
|
.parse(binaryParser as unknown as ParseCallback)
|
|
.expect(200);
|
|
expect(openOrphan.headers['content-disposition']).toContain('filename="lose-datei.pdf"');
|
|
});
|
|
|
|
it('blocks uploads to classified pages server-side when the policy says so (issue #213)', async () => {
|
|
const settings = app.get(InstanceSettingsService);
|
|
const page = await api()
|
|
.post(`/api/v1/ponds/${pondId}/pages`)
|
|
.set('Cookie', ownerCookie)
|
|
.send({ title: `Blocked Uploads ${suffix}` })
|
|
.expect(201);
|
|
await prisma.page.update({
|
|
where: { id: page.body.id as string },
|
|
data: { classification: 'VS_NFD' },
|
|
});
|
|
|
|
// Default policy `warn`: the upload is allowed (the UI shows the notice).
|
|
await api()
|
|
.post(`/api/v1/pages/${page.body.id}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', Buffer.from('%PDF-1.4 warned upload'), 'warned.pdf')
|
|
.expect(201);
|
|
|
|
await settings.set('classification.uploadPolicy', 'block', 'test');
|
|
try {
|
|
// Enforced server-side, not only in the UI.
|
|
const blocked = await api()
|
|
.post(`/api/v1/pages/${page.body.id}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', Buffer.from('%PDF-1.4 blocked upload'), 'blocked.pdf')
|
|
.expect(403);
|
|
expect((blocked.body as { code: string }).code).toBe('classified_upload_blocked');
|
|
|
|
// Unclassified pages stay uploadable under `block`.
|
|
const open = await api()
|
|
.post(`/api/v1/ponds/${pondId}/pages`)
|
|
.set('Cookie', ownerCookie)
|
|
.send({ title: `Open Uploads ${suffix}` })
|
|
.expect(201);
|
|
await api()
|
|
.post(`/api/v1/pages/${open.body.id}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', Buffer.from('%PDF-1.4 open upload'), 'open.pdf')
|
|
.expect(201);
|
|
} finally {
|
|
await settings.set('classification.uploadPolicy', 'warn', 'test');
|
|
await prisma.instanceSetting.deleteMany({
|
|
where: { key: 'classification.uploadPolicy' },
|
|
});
|
|
}
|
|
});
|
|
|
|
it('pond file manager reports usage, orphans, and page links (#61)', async () => {
|
|
const page = await api()
|
|
.post(`/api/v1/ponds/${pondId}/pages`)
|
|
.set('Cookie', ownerCookie)
|
|
.send({ title: `Manager Page ${suffix}` })
|
|
.expect(201);
|
|
const linked = await api()
|
|
.post(`/api/v1/pages/${page.body.id}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', Buffer.from('%PDF-1.4 linked'), 'linked.pdf')
|
|
.expect(201);
|
|
const orphan = await api()
|
|
.post(`/api/v1/ponds/${pondId}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', pngBuffer('orphan image'), 'orphan.png')
|
|
.expect(201);
|
|
|
|
const manager = await api()
|
|
.get(`/api/v1/ponds/${pondId}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.expect(200);
|
|
expect(manager.body.storageBytesLimit).toBeGreaterThan(0);
|
|
const usage = await prisma.pondUsage.findUnique({ where: { pondId } });
|
|
expect(manager.body.storageBytesUsed).toBe(Number(usage?.storageBytesUsed ?? 0));
|
|
|
|
const linkedItem = manager.body.files.find((f: { id: string }) => f.id === linked.body.id);
|
|
const orphanItem = manager.body.files.find((f: { id: string }) => f.id === orphan.body.id);
|
|
expect(linkedItem.pageTitle).toBe(`Manager Page ${suffix}`);
|
|
expect(orphanItem.pageTitle).toBeNull();
|
|
});
|
|
|
|
it('denies the pond file manager to a non-admin (#61)', async () => {
|
|
await api().get(`/api/v1/ponds/${pondId}/files`).set('Cookie', outsiderCookie).expect(404);
|
|
});
|
|
|
|
it('hides foreign-pond files from download and delete (404, not 403)', async () => {
|
|
const uploaded = await api()
|
|
.post(`/api/v1/ponds/${pondId}/files`)
|
|
.set('Cookie', ownerCookie)
|
|
.attach('file', pngBuffer('private'), 'private.png')
|
|
.expect(201);
|
|
|
|
await api().get(`/api/v1/media/${uploaded.body.id}`).set('Cookie', outsiderCookie).expect(404);
|
|
await api()
|
|
.delete(`/api/v1/files/${uploaded.body.id}`)
|
|
.set('Cookie', outsiderCookie)
|
|
.expect(404);
|
|
await api()
|
|
.post(`/api/v1/ponds/${pondId}/files`)
|
|
.set('Cookie', outsiderCookie)
|
|
.attach('file', pngBuffer('sneaky'), 'sneaky.png')
|
|
.expect(404);
|
|
});
|
|
});
|