feat: Admin Area General Settings and Categories 1.16

This commit is contained in:
OpenVelo Agent
2026-07-22 19:19:44 +00:00
parent 73cfbb9af0
commit c02d3934d4
7 changed files with 163 additions and 109 deletions
+65 -11
View File
@@ -6,6 +6,7 @@ process.env.UPLOAD_SIZE_LIMIT = '1mb';
import * as fs from 'fs';
import * as path from 'path';
import sharp from 'sharp';
import { safeFilename, parseUploadSizeLimit } from '../../backend/src/common/utils/upload';
@@ -124,22 +125,75 @@ describe('Uploads endpoint integration', () => {
expect([401, 403]).toContain(res.status);
});
it('uploads a small file successfully', async () => {
it('uploads a valid image and writes a deterministic 128x128 PNG', async () => {
const { agent, csrf } = await primeCsrf();
const png = await sharp({
create: { width: 64, height: 64, channels: 3, background: { r: 1, g: 2, b: 3 } },
}).png().toBuffer();
const res = await agent
.post('/api/v1/uploads/category-icon')
.set('Authorization', `Bearer ${adminToken}`)
.set('X-CSRF-Token', csrf)
.field('categoryId', 'icon-test-cat')
.attach('file', png, 'icon.png');
expect(res.status).toBe(201);
expect(res.body.id).toBe('icon-test-cat.png');
expect(res.body.publicUrl).toBe('/uploads/icons/icon-test-cat.png');
expect(res.body.width).toBe(128);
expect(res.body.height).toBe(128);
expect(res.body.mimeType).toBe('image/png');
const written = path.join(process.env.UPLOAD_DIR!, 'icons', 'icon-test-cat.png');
expect(fs.existsSync(written)).toBe(true);
const decoded = await sharp(written).metadata();
expect(decoded.format).toBe('png');
expect(decoded.width).toBe(128);
expect(decoded.height).toBe(128);
await request(app.getHttpServer()).get(res.body.publicUrl).expect(200);
});
it('rejects a plain-text payload labeled as a category icon and does not overwrite a valid icon', async () => {
const iconsDir = path.join(process.env.UPLOAD_DIR!, 'icons');
fs.mkdirSync(iconsDir, { recursive: true });
const target = path.join(iconsDir, 'icon-tx-cat.png');
const validPng = await sharp({
create: { width: 32, height: 32, channels: 3, background: { r: 9, g: 9, b: 9 } },
}).png().toBuffer();
fs.writeFileSync(target, validPng);
const beforeBytes = fs.readFileSync(target);
const { agent, csrf } = await primeCsrf();
const res = await agent
.post('/api/v1/uploads/category-icon')
.set('Authorization', `Bearer ${adminToken}`)
.set('X-CSRF-Token', csrf)
.attach('file', Buffer.from('hello world'), 'icon.png');
expect(res.status).toBe(201);
expect(res.body.id).toMatch(/^icon-[0-9a-f]{8}\.png$/);
expect(res.body.publicUrl).toBe(`/uploads/icons/${res.body.id}`);
// File actually written under UPLOAD_DIR/icons
const written = path.join(process.env.UPLOAD_DIR!, 'icons', res.body.id);
expect(fs.existsSync(written)).toBe(true);
// Public-served via /uploads static handler.
const fetched = await request(app.getHttpServer()).get(res.body.publicUrl).expect(200);
expect(fetched.body?.toString()).toBe('hello world');
.field('categoryId', 'icon-tx-cat')
.attach('file', Buffer.from('not an image'), { filename: 'icon.png', contentType: 'image/png' });
expect(res.status).toBe(400);
expect(res.body?.message).toMatch(/valid PNG|JPEG|GIF|WebP/i);
expect(fs.existsSync(target)).toBe(true);
expect(fs.readFileSync(target).equals(beforeBytes)).toBe(true);
});
it('rejects a corrupt PNG payload and does not overwrite a valid icon', async () => {
const iconsDir = path.join(process.env.UPLOAD_DIR!, 'icons');
const target = path.join(iconsDir, 'icon-corrupt-cat.png');
const validPng = await sharp({
create: { width: 32, height: 32, channels: 3, background: { r: 5, g: 5, b: 5 } },
}).png().toBuffer();
fs.writeFileSync(target, validPng);
const beforeBytes = fs.readFileSync(target);
const { agent, csrf } = await primeCsrf();
const corrupt = Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), Buffer.alloc(53, 0x00)]);
const res = await agent
.post('/api/v1/uploads/category-icon')
.set('Authorization', `Bearer ${adminToken}`)
.set('X-CSRF-Token', csrf)
.field('categoryId', 'icon-corrupt-cat')
.attach('file', corrupt, { filename: 'corrupt.png', contentType: 'image/png' });
expect(res.status).toBe(400);
expect(res.body?.message).toMatch(/valid PNG|JPEG|GIF|WebP/i);
expect(fs.readFileSync(target).equals(beforeBytes)).toBe(true);
});
it('rejects oversize uploads (limit=1mb, file=2mb)', async () => {