feat: Admin Area General Settings and Categories 1.16
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
||||
import {
|
||||
syncCategoryForm,
|
||||
CategoryFormMode,
|
||||
CategoryFormSubmit,
|
||||
} from '../../frontend/src/app/features/admin/categories/category-form-modal.component';
|
||||
import { AdminCategory } from '../../frontend/src/app/core/services/admin.service';
|
||||
|
||||
@@ -185,4 +186,36 @@ describe('CategoryFormModalComponent - prefill contract', () => {
|
||||
expect(form.controls.abbreviation.value).toBe('HW');
|
||||
expect(form.controls.description.value).toBe('Hardware challenges');
|
||||
});
|
||||
|
||||
it('keeps user (non-system) abbreviation editable and emits the uppercased abbreviation', () => {
|
||||
const form = buildForm();
|
||||
const result = syncCategoryForm(
|
||||
form,
|
||||
'edit',
|
||||
makeRow({
|
||||
id: 'user-cat',
|
||||
name: 'Cat A Test T007',
|
||||
abbreviation: 'CATA',
|
||||
description: 'd',
|
||||
isSystem: false,
|
||||
systemKey: null,
|
||||
}),
|
||||
);
|
||||
expect(result.abbreviationReadonly).toBe(false);
|
||||
|
||||
form.controls.name.setValue('Cat Z Renamed T007');
|
||||
form.controls.abbreviation.setValue('zzza');
|
||||
form.controls.description.setValue('Renamed to ZZZA for T007');
|
||||
|
||||
const v = form.getRawValue();
|
||||
const emitted: CategoryFormSubmit = {
|
||||
mode: 'edit',
|
||||
name: v.name,
|
||||
abbreviation: v.abbreviation.toUpperCase(),
|
||||
description: v.description,
|
||||
};
|
||||
expect(emitted.abbreviation).toBe('ZZZA');
|
||||
expect(emitted.name).toBe('Cat Z Renamed T007');
|
||||
expect(emitted.description).toBe('Renamed to ZZZA for T007');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user