feat: limity skupinových objednávek nastavitelné přes env
CI / Generate TypeScript types (push) Successful in 12s
CI / Server unit tests (push) Successful in 25s
CI / Build server (push) Successful in 30s
CI / Build client (push) Successful in 39s
CI / Playwright E2E tests (push) Successful in 3m55s
CI / Build and push Docker image (push) Successful in 49s
CI / Notify (push) Successful in 2s

This commit is contained in:
batmanisko committed 2026-09-24 09:18:56 +02:00
1 parent 076b8db179
commit 103445dba8
15 files changed
+386 -28

No files matched your search

+125
View File
@@ -0,0 +1,125 @@
import express from 'express';
import request from 'supertest';
import bodyParser from 'body-parser';
import { generateToken } from '../auth';
import { resetMemoryStorage } from '../storage/memory';
import getStorage from '../storage';
import groupRouter from '../routes/groupRoutes';
import { DEFAULT_GROUP_LIMITS, getGroupLimits, resetGroupLimits } from '../limits';
const MAX_NOTE_LENGTH = DEFAULT_GROUP_LIMITS.maxNoteLength;
const MAX_SURCHARGE_TEXT_LENGTH = DEFAULT_GROUP_LIMITS.maxSurchargeTextLength;
/** Maximum částky v haléřích */
const MAX_AMOUNT = DEFAULT_GROUP_LIMITS.maxAmountKc * 100;
jest.mock('../websocket', () => ({ getWebsocket: () => ({ emit: jest.fn() }) }));
const CREATOR = 'kreator';
const TOKEN = `Bearer ${generateToken(CREATOR)}`;
function buildApp() {
const app = express();
app.use(bodyParser.json());
app.use('/api/groups', groupRouter);
app.use((err: any, _req: any, res: any, _next: any) => {
res.status(500).json({ error: err.message });
});
return app;
}
/** Založí skupinu a vrátí její ID. */
async function createTestGroup(app: express.Express): Promise<string> {
const res = await request(app).post('/api/groups/create').set('Authorization', TOKEN).send({ name: 'Bistro' });
expect(res.status).toBe(200);
return res.body.groups[0].id;
}
beforeEach(async () => {
resetMemoryStorage();
resetGroupLimits();
await getStorage().setData('stores', [{ name: 'Bistro' }]);
});
describe('limity vstupů skupinových objednávek', () => {
test('poznámka a popis příplatku mají maximální délku', async () => {
const app = buildApp();
const id = await createTestGroup(app);
const post = (body: object) => request(app).post('/api/groups/updateMember').set('Authorization', TOKEN).send({ id, login: CREATOR, ...body });
expect((await post({ note: 'x'.repeat(MAX_NOTE_LENGTH) })).status).toBe(200);
const tooLong = await post({ note: 'x'.repeat(MAX_NOTE_LENGTH + 1) });
expect(tooLong.status).toBe(400);
expect(tooLong.body.error).toContain(`${MAX_NOTE_LENGTH} znaků`);
expect((await post({ surchargeText: 'x'.repeat(MAX_SURCHARGE_TEXT_LENGTH + 1) })).status).toBe(400);
});
test('částka a příplatek mají maximální výši', async () => {
const app = buildApp();
const id = await createTestGroup(app);
const post = (body: object) => request(app).post('/api/groups/updateMember').set('Authorization', TOKEN).send({ id, login: CREATOR, ...body });
expect((await post({ amount: MAX_AMOUNT })).status).toBe(200);
const tooMuch = await post({ amount: MAX_AMOUNT + 1 });
expect(tooMuch.status).toBe(400);
expect(tooMuch.body.error).toContain('10000 Kč');
expect((await post({ surchargeAmount: MAX_AMOUNT + 1 })).status).toBe(400);
expect((await post({ amount: -1 })).status).toBe(400);
});
test('poplatky mají maximum a procentní sleva nesmí přesáhnout 100 %', async () => {
const app = buildApp();
const id = await createTestGroup(app);
const post = (body: object) => request(app).post('/api/groups/updateFees').set('Authorization', TOKEN).send({ id, ...body });
expect((await post({ shipping: MAX_AMOUNT + 1 })).status).toBe(400);
expect((await post({ discountType: 'percent', discountValue: 101 })).status).toBe(400);
expect((await post({ discountType: 'percent', discountValue: 100 })).status).toBe(200);
// Jen hodnota při již nastavené procentní slevě — pojistka v groups.ts
const onlyValue = await post({ discountValue: 150 });
expect(onlyValue.status).toBe(500);
expect(onlyValue.body.error).toContain('100 %');
expect((await post({ discountType: 'fixed', discountValue: MAX_AMOUNT + 1 })).status).toBe(400);
});
test('název skupiny a odkaz na sledování mají maximální délku', async () => {
const app = buildApp();
expect((await request(app).post('/api/groups/create').set('Authorization', TOKEN).send({ name: 'x'.repeat(101) })).status).toBe(400);
const id = await createTestGroup(app);
const res = await request(app).post('/api/groups/setTracking').set('Authorization', TOKEN).send({ id, shareUrl: 'https://x.cz/' + 'a'.repeat(600) });
expect(res.status).toBe(400);
});
});
describe('limity z proměnných prostředí', () => {
const ENV_KEYS = ['GROUP_MAX_NOTE_LENGTH', 'GROUP_MAX_AMOUNT_KC', 'GROUP_MAX_ITEMS'];
afterEach(() => {
ENV_KEYS.forEach(k => delete process.env[k]);
resetGroupLimits();
});
test('env přepíše výchozí hodnoty a limity se uplatní v API', async () => {
process.env.GROUP_MAX_NOTE_LENGTH = '20';
process.env.GROUP_MAX_AMOUNT_KC = '500';
process.env.GROUP_MAX_ITEMS = '5';
resetGroupLimits();
expect(getGroupLimits()).toEqual({ ...DEFAULT_GROUP_LIMITS, maxNoteLength: 20, maxAmountKc: 500, maxItems: 5 });
const app = buildApp();
const id = await createTestGroup(app);
const post = (body: object) => request(app).post('/api/groups/updateMember').set('Authorization', TOKEN).send({ id, login: CREATOR, ...body });
expect((await post({ note: 'x'.repeat(21) })).body.error).toContain('20 znaků');
expect((await post({ amount: 50001 })).body.error).toContain('500 Kč');
expect((await post({ amount: 50000 })).status).toBe(200);
});
test('neplatná hodnota v env se ignoruje (zůstane výchozí)', () => {
const warn = jest.spyOn(console, 'warn').mockImplementation(() => { });
process.env.GROUP_MAX_NOTE_LENGTH = 'hodně';
process.env.GROUP_MAX_AMOUNT_KC = '-5';
process.env.GROUP_MAX_ITEMS = '999999';
resetGroupLimits();
expect(getGroupLimits()).toEqual(DEFAULT_GROUP_LIMITS);
expect(warn).toHaveBeenCalledTimes(3);
warn.mockRestore();
});
});