feat: možnost editace existujících podniků u objednávání
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
[
|
||||
"Statistiky se nově načtou i pro aktuální týden, který ještě neskončil",
|
||||
"Podnik může mít více odkazů na nabídku (např. Bolt Food, Wolt i Foodora)",
|
||||
"Při vytváření skupiny lze vybrat, přes kterou službu se bude objednávat — odkaz v záhlaví skupiny pak vede přímo na ni"
|
||||
"Při vytváření skupiny lze vybrat, přes kterou službu se bude objednávat — odkaz v záhlaví skupiny pak vede přímo na ni",
|
||||
"Existující podnik lze upravit — přejmenovat i změnit odkazy na nabídku, bez nutnosti smazat a znovu přidat"
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import express from "express";
|
||||
import { getStores, addStore, removeStore } from "../stores";
|
||||
import { getStores, addStore, updateStore, removeStore } from "../stores";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -32,6 +32,31 @@ router.post("/add", async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/update", async (req, res, next) => {
|
||||
const { name, newName, heslo, urls } = 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' });
|
||||
}
|
||||
if (newName != null && typeof newName !== 'string') {
|
||||
return res.status(400).json({ error: 'Neplatný název obchodu' });
|
||||
}
|
||||
if (urls != null && (!Array.isArray(urls) || urls.some((u: unknown) => typeof u !== 'string'))) {
|
||||
return res.status(400).json({ error: 'Neplatný seznam URL obchodu' });
|
||||
}
|
||||
try {
|
||||
const stores = await updateStore(name, heslo, newName ?? undefined, urls ?? undefined);
|
||||
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') {
|
||||
|
||||
@@ -95,6 +95,46 @@ export async function addStore(name: string, heslo: string, urls?: string[]): Pr
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upraví existující obchod — název a/nebo seznam URL na nabídku.
|
||||
*
|
||||
* @param name aktuální název obchodu (identifikuje upravovaný obchod)
|
||||
* @param heslo admin heslo
|
||||
* @param newName nový název; pokud není předán, název zůstane nezměněn
|
||||
* @param urls nový seznam URL (nahradí stávající); pokud není předán, URL zůstanou nezměněné
|
||||
*/
|
||||
export async function updateStore(name: string, heslo: string, newName?: string, urls?: string[]): Promise<Store[]> {
|
||||
const adminPassword = process.env.ADMIN_PASSWORD;
|
||||
if (!adminPassword || heslo !== adminPassword) {
|
||||
throw new Error('UNAUTHORIZED');
|
||||
}
|
||||
const stores = await getStores();
|
||||
const index = stores.findIndex(s => s.name.toLowerCase() === name.trim().toLowerCase());
|
||||
if (index < 0) {
|
||||
throw new Error('Obchod nebyl nalezen');
|
||||
}
|
||||
const current = stores[index];
|
||||
|
||||
let finalName = current.name;
|
||||
if (newName !== undefined) {
|
||||
const trimmedName = newName.trim();
|
||||
if (!trimmedName) {
|
||||
throw new Error('Název obchodu nesmí být prázdný');
|
||||
}
|
||||
// Nový název nesmí kolidovat s jiným obchodem (shoda se sebou samým je v pořádku)
|
||||
if (stores.some((s, i) => i !== index && s.name.toLowerCase() === trimmedName.toLowerCase())) {
|
||||
throw new Error('Obchod s tímto názvem již existuje');
|
||||
}
|
||||
finalName = trimmedName;
|
||||
}
|
||||
|
||||
const finalUrls = urls !== undefined ? normalizeUrls(urls) : (current.urls ?? []);
|
||||
const updatedStore: Store = finalUrls.length > 0 ? { name: finalName, urls: finalUrls } : { name: finalName };
|
||||
const updated = stores.map((s, i) => (i === index ? updatedStore : s));
|
||||
await storage.setData(STORES_KEY, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Odebere obchod ze seznamu povolených (dle názvu).
|
||||
*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import getStorage from '../storage';
|
||||
import { getStores, addStore, removeStore } from '../stores';
|
||||
import { getStores, addStore, updateStore, removeStore } from '../stores';
|
||||
|
||||
const ADMIN_PW = 'testadmin';
|
||||
|
||||
@@ -113,6 +113,82 @@ describe('addStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateStore', () => {
|
||||
const WOLT_URL = 'https://wolt.com/bistro';
|
||||
const BOLT_URL = 'https://food.bolt.eu/bistro';
|
||||
|
||||
beforeEach(async () => {
|
||||
await addStore('Bistro', ADMIN_PW, [WOLT_URL]);
|
||||
});
|
||||
|
||||
test('přidá další URL k existujícímu obchodu', async () => {
|
||||
const stores = await updateStore('Bistro', ADMIN_PW, undefined, [WOLT_URL, BOLT_URL]);
|
||||
expect(stores).toContainEqual({ name: 'Bistro', urls: [WOLT_URL, BOLT_URL] });
|
||||
});
|
||||
|
||||
test('přejmenuje obchod a zachová URL', async () => {
|
||||
const stores = await updateStore('Bistro', ADMIN_PW, ' Bistro U Nás ');
|
||||
expect(stores).toContainEqual({ name: 'Bistro U Nás', urls: [WOLT_URL] });
|
||||
});
|
||||
|
||||
test('prázdné pole URL odstraní všechny URL', async () => {
|
||||
const stores = await updateStore('Bistro', ADMIN_PW, undefined, []);
|
||||
expect(stores).toContainEqual({ name: 'Bistro' });
|
||||
});
|
||||
|
||||
test('bez předaných URL zůstanou URL nezměněné', async () => {
|
||||
const stores = await updateStore('Bistro', ADMIN_PW, 'Bistro 2');
|
||||
expect(stores).toContainEqual({ name: 'Bistro 2', urls: [WOLT_URL] });
|
||||
});
|
||||
|
||||
test('zachová pozici obchodu v seznamu', async () => {
|
||||
await addStore('KFC', ADMIN_PW);
|
||||
const stores = await updateStore('Bistro', ADMIN_PW, 'Bistro 2');
|
||||
expect(names(stores)).toEqual(['Bistro 2', 'KFC']);
|
||||
});
|
||||
|
||||
test('funguje case-insensitive dle názvu', async () => {
|
||||
const stores = await updateStore('BISTRO', ADMIN_PW, undefined, [BOLT_URL]);
|
||||
expect(stores).toContainEqual({ name: 'Bistro', urls: [BOLT_URL] });
|
||||
});
|
||||
|
||||
test('ponechání stejného názvu není bráno jako duplikát', async () => {
|
||||
const stores = await updateStore('Bistro', ADMIN_PW, 'bistro');
|
||||
expect(names(stores)).toEqual(['bistro']);
|
||||
});
|
||||
|
||||
test('odmítne název kolidující s jiným obchodem', async () => {
|
||||
await addStore('KFC', ADMIN_PW);
|
||||
await expect(updateStore('Bistro', ADMIN_PW, 'kfc')).rejects.toThrow('existuje');
|
||||
});
|
||||
|
||||
test('odmítne prázdný název', async () => {
|
||||
await expect(updateStore('Bistro', ADMIN_PW, ' ')).rejects.toThrow('prázdný');
|
||||
});
|
||||
|
||||
test('odmítne nevalidní URL', async () => {
|
||||
await expect(updateStore('Bistro', ADMIN_PW, undefined, ['javascript:alert(1)'])).rejects.toThrow('http');
|
||||
});
|
||||
|
||||
test('vyhodí chybu u neexistujícího obchodu', async () => {
|
||||
await expect(updateStore('Neexistuje', ADMIN_PW, 'Nový')).rejects.toThrow('nebyl nalezen');
|
||||
});
|
||||
|
||||
test('vyhodí UNAUTHORIZED s nesprávným heslem', async () => {
|
||||
await expect(updateStore('Bistro', 'spatne', 'Hacknuto')).rejects.toThrow('UNAUTHORIZED');
|
||||
});
|
||||
|
||||
test('vyhodí UNAUTHORIZED pokud ADMIN_PASSWORD není nastaven', async () => {
|
||||
delete process.env.ADMIN_PASSWORD;
|
||||
await expect(updateStore('Bistro', '', 'Hacknuto')).rejects.toThrow('UNAUTHORIZED');
|
||||
});
|
||||
|
||||
test('při nesprávném heslu se obchod nezmění', async () => {
|
||||
await expect(updateStore('Bistro', 'spatne', 'Hacknuto')).rejects.toThrow('UNAUTHORIZED');
|
||||
expect(names(await getStores())).toEqual(['Bistro']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeStore', () => {
|
||||
beforeEach(async () => {
|
||||
await addStore('McDonald\'s', ADMIN_PW);
|
||||
|
||||
Reference in New Issue
Block a user