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,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);
|
||||
}
|
||||
@@ -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'));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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