feat: /objednani – skupinové objednávky s QR platbou
CI / Generate TypeScript types (push) Successful in 18s
CI / Generate TypeScript types (pull_request) Successful in 9s
CI / Server unit tests (push) Successful in 21s
CI / Build client (push) Successful in 40s
CI / Server unit tests (pull_request) Successful in 21s
CI / Build server (pull_request) Successful in 24s
CI / Build server (push) Has been cancelled
CI / Playwright E2E tests (push) Has been cancelled
CI / Build and push Docker image (push) Has been cancelled
CI / Notify (push) Has been cancelled
CI / Build client (pull_request) Has been cancelled
CI / Playwright E2E tests (pull_request) Has been cancelled
CI / Build and push Docker image (pull_request) Has been cancelled
CI / Notify (pull_request) Has been cancelled
CI / Generate TypeScript types (push) Successful in 18s
CI / Generate TypeScript types (pull_request) Successful in 9s
CI / Server unit tests (push) Successful in 21s
CI / Build client (push) Successful in 40s
CI / Server unit tests (pull_request) Successful in 21s
CI / Build server (pull_request) Successful in 24s
CI / Build server (push) Has been cancelled
CI / Playwright E2E tests (push) Has been cancelled
CI / Build and push Docker image (push) Has been cancelled
CI / Notify (push) Has been cancelled
CI / Build client (pull_request) Has been cancelled
CI / Playwright E2E tests (pull_request) Has been cancelled
CI / Build and push Docker image (pull_request) Has been cancelled
CI / Notify (pull_request) Has been cancelled
Nahrazuje /vecere novou stránkou /objednani. Místo jednoho OBJEDNAVAM bucketu umožňuje vytvářet více skupin, kde každá objednává z jiného obchodu. - Skupiny mají stavový automat: open → locked → ordered - Obchody spravuje admin heslem (ADMIN_PASSWORD env var) přes modal „Správa obchodů" - Při stavu ordered zakladatel generuje QR kódy platby (nový PayForGroupModal – volné částky bez menu) - PayForAllModal (oběd) upraven: plátce nyní vidí svůj vlastní díl jako informační řádek - Nové testy: stores.test.ts + groups.test.ts (36 testů)
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import { getStores, addStore } from '../stores';
|
||||
import { createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState } from '../groups';
|
||||
import { GroupState } from '../../../types/gen/types.gen';
|
||||
|
||||
const CREATOR = 'tomas';
|
||||
const USER = 'petr';
|
||||
const ADMIN_PW = 'testadmin';
|
||||
const STORE = 'McDonald\'s';
|
||||
const TODAY = new Date('2025-01-10');
|
||||
|
||||
beforeEach(async () => {
|
||||
resetMemoryStorage();
|
||||
process.env.ADMIN_PASSWORD = ADMIN_PW;
|
||||
await addStore(STORE, ADMIN_PW);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.ADMIN_PASSWORD;
|
||||
});
|
||||
|
||||
describe('createGroup', () => {
|
||||
test('vytvoří skupinu, creator je člen', async () => {
|
||||
const data = await createGroup(CREATOR, STORE, TODAY);
|
||||
expect(data.groups).toHaveLength(1);
|
||||
const group = data.groups![0];
|
||||
expect(group.name).toBe(STORE);
|
||||
expect(group.creatorLogin).toBe(CREATOR);
|
||||
expect(group.state).toBe(GroupState.OPEN);
|
||||
expect(group.members[CREATOR]).toBeDefined();
|
||||
});
|
||||
|
||||
test('odmítne název mimo seznam obchodů', async () => {
|
||||
await expect(createGroup(CREATOR, 'Neznámý obchod', TODAY)).rejects.toThrow('seznam');
|
||||
});
|
||||
|
||||
test('vygeneruje unikátní ID', async () => {
|
||||
const d1 = await createGroup(CREATOR, STORE, TODAY);
|
||||
const d2 = await createGroup(USER, STORE, TODAY);
|
||||
expect(d2.groups).toHaveLength(2);
|
||||
expect(d2.groups![1].id).not.toBe(d2.groups![0].id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteGroup', () => {
|
||||
test('creator může smazat skupinu', async () => {
|
||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||
const groupId = d.groups![0].id;
|
||||
const result = await deleteGroup(CREATOR, groupId, TODAY);
|
||||
expect(result.groups).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('nečlen nemůže smazat skupinu', async () => {
|
||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||
const groupId = d.groups![0].id;
|
||||
await expect(deleteGroup(USER, groupId, TODAY)).rejects.toThrow('zakladatel');
|
||||
});
|
||||
|
||||
test('smazání neexistující skupiny vyhodí chybu', async () => {
|
||||
await expect(deleteGroup(CREATOR, 'nonexistent', TODAY)).rejects.toThrow('nalezena');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addGroupMember', () => {
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||
groupId = d.groups![0].id;
|
||||
});
|
||||
|
||||
test('uživatel se může přidat sám (open)', async () => {
|
||||
const d = await addGroupMember(USER, groupId, USER, TODAY);
|
||||
expect(d.groups![0].members[USER]).toBeDefined();
|
||||
});
|
||||
|
||||
test('creator může přidat jiného uživatele', async () => {
|
||||
const d = await addGroupMember(CREATOR, groupId, USER, TODAY);
|
||||
expect(d.groups![0].members[USER]).toBeDefined();
|
||||
});
|
||||
|
||||
test('nečlen nemůže přidat jiného uživatele', async () => {
|
||||
await expect(addGroupMember(USER, groupId, 'dalsi', TODAY)).rejects.toThrow('zakladatel');
|
||||
});
|
||||
|
||||
test('nelze přidat do skupiny ve stavu ordered', async () => {
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||
await expect(addGroupMember(USER, groupId, USER, TODAY)).rejects.toThrow('objednáno');
|
||||
});
|
||||
|
||||
test('nelze přidat existujícího člena', async () => {
|
||||
await expect(addGroupMember(CREATOR, groupId, CREATOR, TODAY)).rejects.toThrow('již');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeGroupMember', () => {
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||
groupId = d.groups![0].id;
|
||||
await addGroupMember(CREATOR, groupId, USER, TODAY);
|
||||
});
|
||||
|
||||
test('člen se může odhlásit sám', async () => {
|
||||
const d = await removeGroupMember(USER, groupId, USER, TODAY);
|
||||
expect(d.groups![0].members[USER]).toBeUndefined();
|
||||
});
|
||||
|
||||
test('creator může odebrat jiného člena', async () => {
|
||||
const d = await removeGroupMember(CREATOR, groupId, USER, TODAY);
|
||||
expect(d.groups![0].members[USER]).toBeUndefined();
|
||||
});
|
||||
|
||||
test('nelze odebrat zakladatele', async () => {
|
||||
await expect(removeGroupMember(CREATOR, groupId, CREATOR, TODAY)).rejects.toThrow('Zakladatel');
|
||||
});
|
||||
|
||||
test('nečlen nemůže odebrat jiného', async () => {
|
||||
await expect(removeGroupMember(USER, groupId, CREATOR, TODAY)).rejects.toThrow('zakladatel');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateGroupMember', () => {
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||
groupId = d.groups![0].id;
|
||||
await addGroupMember(CREATOR, groupId, USER, TODAY);
|
||||
});
|
||||
|
||||
test('člen může upravit svá data (open)', async () => {
|
||||
const d = await updateGroupMember(USER, groupId, USER, { amount: 150, note: 'Big Mac' }, TODAY);
|
||||
expect(d.groups![0].members[USER].amount).toBe(150);
|
||||
expect(d.groups![0].members[USER].note).toBe('Big Mac');
|
||||
});
|
||||
|
||||
test('creator může upravit data jiného člena', async () => {
|
||||
const d = await updateGroupMember(CREATOR, groupId, USER, { amount: 200 }, TODAY);
|
||||
expect(d.groups![0].members[USER].amount).toBe(200);
|
||||
});
|
||||
|
||||
test('člen nemůže upravit data jiného (locked)', async () => {
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
await expect(updateGroupMember(USER, groupId, USER, { amount: 100 }, TODAY)).rejects.toThrow('uzamčeno');
|
||||
});
|
||||
|
||||
test('nikdo nemůže upravit při stavu ordered', async () => {
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||
await expect(updateGroupMember(CREATOR, groupId, USER, { amount: 100 }, TODAY)).rejects.toThrow('objednáno');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setGroupState', () => {
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||
groupId = d.groups![0].id;
|
||||
});
|
||||
|
||||
test('open → locked', async () => {
|
||||
const d = await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
expect(d.groups![0].state).toBe(GroupState.LOCKED);
|
||||
});
|
||||
|
||||
test('locked → open (odemčení)', async () => {
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
const d = await setGroupState(CREATOR, groupId, GroupState.OPEN, TODAY);
|
||||
expect(d.groups![0].state).toBe(GroupState.OPEN);
|
||||
});
|
||||
|
||||
test('locked → ordered', async () => {
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
const d = await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||
expect(d.groups![0].state).toBe(GroupState.ORDERED);
|
||||
});
|
||||
|
||||
test('open → ordered není povoleno', async () => {
|
||||
await expect(setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY)).rejects.toThrow('Nelze přejít');
|
||||
});
|
||||
|
||||
test('ordered je terminální stav', async () => {
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||
await expect(setGroupState(CREATOR, groupId, GroupState.OPEN, TODAY)).rejects.toThrow('Nelze přejít');
|
||||
});
|
||||
|
||||
test('nečlen nemůže měnit stav', async () => {
|
||||
await expect(setGroupState(USER, groupId, GroupState.LOCKED, TODAY)).rejects.toThrow('zakladatel');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import { getStores, addStore, removeStore } from '../stores';
|
||||
|
||||
const ADMIN_PW = 'testadmin';
|
||||
|
||||
beforeEach(() => {
|
||||
resetMemoryStorage();
|
||||
process.env.ADMIN_PASSWORD = ADMIN_PW;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.ADMIN_PASSWORD;
|
||||
});
|
||||
|
||||
describe('getStores', () => {
|
||||
test('vrátí prázdné pole, pokud seznam neexistuje', async () => {
|
||||
const stores = await getStores();
|
||||
expect(stores).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addStore', () => {
|
||||
test('přidá obchod se správným heslem', async () => {
|
||||
const stores = await addStore('McDonald\'s', ADMIN_PW);
|
||||
expect(stores).toContain('McDonald\'s');
|
||||
});
|
||||
|
||||
test('vyhodí UNAUTHORIZED s nesprávným heslem', async () => {
|
||||
await expect(addStore('KFC', 'spatne')).rejects.toThrow('UNAUTHORIZED');
|
||||
});
|
||||
|
||||
test('vyhodí UNAUTHORIZED pokud ADMIN_PASSWORD není nastaven', async () => {
|
||||
delete process.env.ADMIN_PASSWORD;
|
||||
await expect(addStore('KFC', '')).rejects.toThrow('UNAUTHORIZED');
|
||||
});
|
||||
|
||||
test('odmítne prázdný název', async () => {
|
||||
await expect(addStore(' ', ADMIN_PW)).rejects.toThrow('prázdný');
|
||||
});
|
||||
|
||||
test('odmítne duplikát (case-insensitive)', async () => {
|
||||
await addStore('McDonald\'s', ADMIN_PW);
|
||||
await expect(addStore('mcdonald\'s', ADMIN_PW)).rejects.toThrow('existuje');
|
||||
});
|
||||
|
||||
test('vrátí aktualizovaný seznam', async () => {
|
||||
await addStore('McDonald\'s', ADMIN_PW);
|
||||
const stores = await addStore('KFC', ADMIN_PW);
|
||||
expect(stores).toHaveLength(2);
|
||||
expect(stores).toContain('McDonald\'s');
|
||||
expect(stores).toContain('KFC');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeStore', () => {
|
||||
beforeEach(async () => {
|
||||
await addStore('McDonald\'s', ADMIN_PW);
|
||||
});
|
||||
|
||||
test('odebere obchod se správným heslem', async () => {
|
||||
const stores = await removeStore('McDonald\'s', ADMIN_PW);
|
||||
expect(stores).not.toContain('McDonald\'s');
|
||||
});
|
||||
|
||||
test('case-insensitive odebrání', async () => {
|
||||
const stores = await removeStore('MCDONALD\'S', ADMIN_PW);
|
||||
expect(stores).not.toContain('McDonald\'s');
|
||||
});
|
||||
|
||||
test('vyhodí UNAUTHORIZED s nesprávným heslem', async () => {
|
||||
await expect(removeStore('McDonald\'s', 'spatne')).rejects.toThrow('UNAUTHORIZED');
|
||||
});
|
||||
|
||||
test('odebrání neexistujícího obchodu nic nerozbije', async () => {
|
||||
const stores = await removeStore('Neexistuje', ADMIN_PW);
|
||||
expect(stores).toContain('McDonald\'s');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user