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

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:
2026-05-07 07:05:01 +02:00
parent 774be3df6d
commit 936b33cc80
28 changed files with 1641 additions and 242 deletions
+119
View File
@@ -0,0 +1,119 @@
import crypto from "crypto";
import getStorage from "./storage";
import { getClientData, getToday, initIfNeeded } from "./service";
import { getStores } from "./stores";
import { ClientData, GroupState, MealSlot, OrderGroup, OrderGroupMember } from "../../types/gen/types.gen";
import { formatDate } from "./utils";
const storage = getStorage();
async function getExtraData(date?: Date): Promise<ClientData> {
await initIfNeeded(date, MealSlot.EXTRA);
return getClientData(date, MealSlot.EXTRA);
}
function getExtraKey(date?: Date): string {
return `${formatDate(date ?? getToday())}_extra`;
}
async function saveExtraData(data: ClientData, date?: Date): Promise<ClientData> {
await storage.setData(getExtraKey(date), data);
return data;
}
function findGroup(data: ClientData, id: string): OrderGroup | undefined {
return data.groups?.find(g => g.id === id);
}
export async function createGroup(creatorLogin: string, name: string, date?: Date): Promise<ClientData> {
const stores = await getStores();
if (!stores.some(s => s.toLowerCase() === name.trim().toLowerCase())) {
throw new Error('Obchod není v seznamu povolených obchodů');
}
const data = await getExtraData(date);
const group: OrderGroup = {
id: crypto.randomUUID(),
name: name.trim(),
creatorLogin,
state: GroupState.OPEN,
members: { [creatorLogin]: {} },
};
data.groups = [...(data.groups ?? []), group];
return saveExtraData(data, date);
}
export async function deleteGroup(login: string, groupId: string, date?: Date): Promise<ClientData> {
const data = await getExtraData(date);
const group = findGroup(data, groupId);
if (!group) throw new Error('Skupina nebyla nalezena');
if (group.creatorLogin !== login) throw new Error('Skupinu může smazat pouze zakladatel');
data.groups = (data.groups ?? []).filter(g => g.id !== groupId);
return saveExtraData(data, date);
}
export async function addGroupMember(login: string, groupId: string, targetLogin: string, date?: Date): Promise<ClientData> {
const data = await getExtraData(date);
const group = findGroup(data, groupId);
if (!group) throw new Error('Skupina nebyla nalezena');
if (group.state === GroupState.ORDERED) throw new Error('Skupinu ve stavu "objednáno" nelze upravovat');
if (login !== group.creatorLogin && login !== targetLogin) {
throw new Error('Přidat jiného uživatele může pouze zakladatel');
}
if (group.state === GroupState.LOCKED && login !== group.creatorLogin) {
throw new Error('Skupinu ve stavu "uzamčeno" může upravovat pouze zakladatel');
}
if (group.members[targetLogin]) throw new Error('Uživatel je již ve skupině');
group.members[targetLogin] = {};
return saveExtraData(data, date);
}
export async function removeGroupMember(login: string, groupId: string, targetLogin: string, date?: Date): Promise<ClientData> {
const data = await getExtraData(date);
const group = findGroup(data, groupId);
if (!group) throw new Error('Skupina nebyla nalezena');
if (group.state === GroupState.ORDERED) throw new Error('Skupinu ve stavu "objednáno" nelze upravovat');
if (login !== group.creatorLogin && login !== targetLogin) {
throw new Error('Odebrat jiného uživatele může pouze zakladatel');
}
if (group.state === GroupState.LOCKED && login !== group.creatorLogin) {
throw new Error('Skupinu ve stavu "uzamčeno" může upravovat pouze zakladatel');
}
if (targetLogin === group.creatorLogin) throw new Error('Zakladatel skupiny nemůže být odebrán');
if (!group.members[targetLogin]) throw new Error('Uživatel není ve skupině');
delete group.members[targetLogin];
return saveExtraData(data, date);
}
export async function updateGroupMember(login: string, groupId: string, targetLogin: string, patch: Partial<OrderGroupMember>, date?: Date): Promise<ClientData> {
const data = await getExtraData(date);
const group = findGroup(data, groupId);
if (!group) throw new Error('Skupina nebyla nalezena');
if (group.state === GroupState.ORDERED) throw new Error('Skupinu ve stavu "objednáno" nelze upravovat');
const isSelf = login === targetLogin;
const isCreator = login === group.creatorLogin;
if (!isSelf && !isCreator) throw new Error('Upravit jiného uživatele může pouze zakladatel');
if (!isCreator && group.state === GroupState.LOCKED) {
throw new Error('Skupinu ve stavu "uzamčeno" může upravovat pouze zakladatel');
}
if (!group.members[targetLogin]) throw new Error('Uživatel není ve skupině');
group.members[targetLogin] = { ...group.members[targetLogin], ...patch };
return saveExtraData(data, date);
}
const VALID_TRANSITIONS: Record<GroupState, GroupState[]> = {
[GroupState.OPEN]: [GroupState.LOCKED],
[GroupState.LOCKED]: [GroupState.OPEN, GroupState.ORDERED],
[GroupState.ORDERED]: [],
};
export async function setGroupState(login: string, groupId: string, newState: GroupState, date?: Date): Promise<ClientData> {
const data = await getExtraData(date);
const group = findGroup(data, groupId);
if (!group) throw new Error('Skupina nebyla nalezena');
if (group.creatorLogin !== login) throw new Error('Stav může měnit pouze zakladatel');
if (!VALID_TRANSITIONS[group.state].includes(newState)) {
throw new Error(`Nelze přejít ze stavu "${group.state}" do stavu "${newState}"`);
}
group.state = newState;
return saveExtraData(data, date);
}
+4
View File
@@ -21,6 +21,8 @@ import notificationRoutes from "./routes/notificationRoutes";
import qrRoutes from "./routes/qrRoutes";
import devRoutes from "./routes/devRoutes";
import changelogRoutes from "./routes/changelogRoutes";
import groupRoutes from "./routes/groupRoutes";
import storeRoutes from "./routes/storeRoutes";
const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
@@ -180,6 +182,8 @@ app.use("/api/notifications", notificationRoutes);
app.use("/api/qr", qrRoutes);
app.use("/api/dev", devRoutes);
app.use("/api/changelogs", changelogRoutes);
app.use("/api/groups", groupRoutes);
app.use("/api/stores", storeRoutes);
app.use('/stats', express.static('public'));
app.use(express.static('public'));
+1 -1
View File
@@ -71,7 +71,7 @@ const parseValidateFutureDayIndex = (req: Request<{}, any, AddChoiceData["body"]
const parseSlot = (body: Record<string, any>): MealSlot | undefined => {
const slot = body?.slot;
if (slot != null && slot !== MealSlot.OBED && slot !== MealSlot.EXTRA) {
if (slot != null && slot !== MealSlot.OBED) {
throw Error(`Neplatný slot: ${slot}`);
}
return slot ?? undefined;
+93
View File
@@ -0,0 +1,93 @@
import express, { Request } from "express";
import { getLogin } from "../auth";
import { parseToken } from "../utils";
import { getWebsocket } from "../websocket";
import { createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState } from "../groups";
import { GroupState } from "../../../types/gen/types.gen";
const router = express.Router();
function broadcastExtra(data: any) {
getWebsocket().emit("message", data);
}
router.post("/create", async (req: Request, res, next) => {
const login = getLogin(parseToken(req));
const { name } = req.body ?? {};
if (!name || typeof name !== 'string') {
return res.status(400).json({ error: 'Nebyl předán název skupiny' });
}
try {
const data = await createGroup(login, name);
broadcastExtra(data);
res.status(200).json(data);
} catch (e: any) { next(e); }
});
router.post("/delete", async (req: Request, res, next) => {
const login = getLogin(parseToken(req));
const { id } = req.body ?? {};
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
try {
const data = await deleteGroup(login, id);
broadcastExtra(data);
res.status(200).json(data);
} catch (e: any) { next(e); }
});
router.post("/addMember", async (req: Request, res, next) => {
const login = getLogin(parseToken(req));
const { id, login: targetLogin } = req.body ?? {};
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
const target = targetLogin ?? login;
try {
const data = await addGroupMember(login, id, target);
broadcastExtra(data);
res.status(200).json(data);
} catch (e: any) { next(e); }
});
router.post("/removeMember", async (req: Request, res, next) => {
const login = getLogin(parseToken(req));
const { id, login: targetLogin } = req.body ?? {};
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
if (!targetLogin) return res.status(400).json({ error: 'Nebyl předán login uživatele' });
try {
const data = await removeGroupMember(login, id, targetLogin);
broadcastExtra(data);
res.status(200).json(data);
} catch (e: any) { next(e); }
});
router.post("/updateMember", async (req: Request, res, next) => {
const login = getLogin(parseToken(req));
const { id, login: targetLogin, amount, note, surchargeText, surchargeAmount } = req.body ?? {};
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
if (!targetLogin) return res.status(400).json({ error: 'Nebyl předán login uživatele' });
const patch: Record<string, any> = {};
if (amount !== undefined) patch.amount = amount;
if (note !== undefined) patch.note = note;
if (surchargeText !== undefined) patch.surchargeText = surchargeText;
if (surchargeAmount !== undefined) patch.surchargeAmount = surchargeAmount;
try {
const data = await updateGroupMember(login, id, targetLogin, patch);
broadcastExtra(data);
res.status(200).json(data);
} catch (e: any) { next(e); }
});
router.post("/setState", async (req: Request, res, next) => {
const login = getLogin(parseToken(req));
const { id, state } = req.body ?? {};
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
if (!state || !Object.values(GroupState).includes(state)) {
return res.status(400).json({ error: 'Neplatný stav skupiny' });
}
try {
const data = await setGroupState(login, id, state as GroupState);
broadcastExtra(data);
res.status(200).json(data);
} catch (e: any) { next(e); }
});
export default router;
+51
View File
@@ -0,0 +1,51 @@
import express from "express";
import { getStores, addStore, removeStore } from "../stores";
const router = express.Router();
router.get("/", async (_req, res, next) => {
try {
const stores = await getStores();
res.status(200).json(stores);
} catch (e: any) { next(e); }
});
router.post("/add", async (req, res, next) => {
const { name, heslo } = req.body ?? {};
if (!name || typeof name !== 'string') {
return res.status(400).json({ error: 'Nebyl předán název obchodu' });
}
if (!heslo || typeof heslo !== 'string') {
return res.status(400).json({ error: 'Nebylo předáno heslo' });
}
try {
const stores = await addStore(name, heslo);
res.status(200).json(stores);
} catch (e: any) {
if (e.message === 'UNAUTHORIZED') {
return res.status(403).json({ error: 'Nesprávné heslo' });
}
next(e);
}
});
router.post("/delete", async (req, res, next) => {
const { name, heslo } = req.body ?? {};
if (!name || typeof name !== 'string') {
return res.status(400).json({ error: 'Nebyl předán název obchodu' });
}
if (!heslo || typeof heslo !== 'string') {
return res.status(400).json({ error: 'Nebylo předáno heslo' });
}
try {
const stores = await removeStore(name, heslo);
res.status(200).json(stores);
} catch (e: any) {
if (e.message === 'UNAUTHORIZED') {
return res.status(403).json({ error: 'Nesprávné heslo' });
}
next(e);
}
});
export default router;
+4 -1
View File
@@ -3,6 +3,7 @@ import getStorage from "./storage";
import { getMenuSladovnicka, getMenuTechTower, getMenuZastavkaUmichala, getMenuSenkSerikova, StaleWeekError } from "./restaurants";
import { getTodayMock } from "./mock";
import { removeAllUserPizzas } from "./pizza";
import { getStores } from "./stores";
import { ClientData, DepartureTime, LunchChoice, MealSlot, PizzaDayState, Restaurant, RestaurantDayMenu, WeekMenu } from "../../types/gen/types.gen";
const storage = getStorage();
@@ -50,7 +51,9 @@ export function getEmptyData(date?: Date): ClientData {
*/
export async function getData(date?: Date, slot?: MealSlot): Promise<ClientData> {
const clientData = await getClientData(date, slot);
if (slot !== MealSlot.EXTRA) {
if (slot === MealSlot.EXTRA) {
clientData.stores = await getStores();
} else {
clientData.menus = {
SLADOVNICKA: await getRestaurantMenu('SLADOVNICKA', date),
// UMOTLIKU: await getRestaurantMenu('UMOTLIKU', date),
+37
View File
@@ -0,0 +1,37 @@
import getStorage from "./storage";
const storage = getStorage();
const STORES_KEY = 'stores';
export async function getStores(): Promise<string[]> {
return (await storage.getData<string[]>(STORES_KEY)) ?? [];
}
export async function addStore(name: string, heslo: string): Promise<string[]> {
const adminPassword = process.env.ADMIN_PASSWORD;
if (!adminPassword || heslo !== adminPassword) {
throw new Error('UNAUTHORIZED');
}
const trimmed = name.trim();
if (!trimmed) {
throw new Error('Název obchodu nesmí být prázdný');
}
const stores = await getStores();
if (stores.some(s => s.toLowerCase() === trimmed.toLowerCase())) {
throw new Error('Obchod s tímto názvem již existuje');
}
const updated = [...stores, trimmed];
await storage.setData(STORES_KEY, updated);
return updated;
}
export async function removeStore(name: string, heslo: string): Promise<string[]> {
const adminPassword = process.env.ADMIN_PASSWORD;
if (!adminPassword || heslo !== adminPassword) {
throw new Error('UNAUTHORIZED');
}
const stores = await getStores();
const updated = stores.filter(s => s.toLowerCase() !== name.trim().toLowerCase());
await storage.setData(STORES_KEY, updated);
return updated;
}
+195
View File
@@ -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');
});
});
+78
View File
@@ -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');
});
});