feat: přidání testů – Jest unit testy + Playwright E2E + CI pipeline
ci/woodpecker/push/workflow Pipeline was canceled
ci/woodpecker/pr/workflow Pipeline failed

Server:
- Jest unit testy (88 testů): auth, utils, restaurants, service, voting, pizza
- in-memory storage mock pro izolaci testů
- oprava race condition při inicializaci Redis (storageReady promise)
- dev route dostupná i pro NODE_ENV=test
- getStatsMock deterministický (nahrazení Math.random)
- exporty interních helperů pro testovatelnost
- /api/health endpoint pro Playwright readiness check
- tsconfig vylučuje test soubory z produkčního buildu

E2E (e2e/):
- Playwright s Firefoxem + Chromiem
- testy: login, menu, výběr jídla, pizza day životní cyklus, QR/nastavení
- trusted-header auth bypass pro testy, video + trace při selhání

CI (Woodpecker):
- pipeline spouštěna na všech větvích a PR (nejen master)
- redis-stack-server service pro E2E – čistý Redis per větev automaticky
- kroky: unit testy, build, E2E testy (parallel kde možné)
- Docker build zůstává pouze pro master

Co-Authored-By: Claude Opus (extra usage) 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-29 00:25:10 +02:00
parent 1e1e23df80
commit fe6bb3290e
29 changed files with 1136 additions and 42 deletions
+66
View File
@@ -0,0 +1,66 @@
import { FeatureRequest } from '../../../types/gen/types.gen';
const mockStorageData = new Map<string, any>();
jest.mock('../storage', () => ({
__esModule: true,
default: () => ({
hasData: async (key: string) => mockStorageData.has(key),
getData: async <T>(key: string) => mockStorageData.get(key) as T,
setData: async <T>(key: string, val: T) => void mockStorageData.set(key, val),
}),
storageReady: Promise.resolve(),
}));
import { updateFeatureVote, getVotingStats } from '../voting';
beforeEach(() => mockStorageData.clear());
describe('updateFeatureVote', () => {
const feat = 'FEATURE_A' as FeatureRequest;
test('přidá hlas pro nového uživatele', async () => {
const result = await updateFeatureVote('alice', feat, true);
expect(result['alice']).toContain(feat);
});
test('vyhodí chybu při duplicitním hlasování', async () => {
await updateFeatureVote('alice', feat, true);
await expect(updateFeatureVote('alice', feat, true)).rejects.toThrow('hlasovali');
});
test('odebere hlas', async () => {
await updateFeatureVote('alice', feat, true);
await updateFeatureVote('alice', feat, false);
const stats = await getVotingStats();
expect(stats[feat] ?? 0).toBe(0);
});
test('odebrání neexistujícího hlasu je no-op', async () => {
await expect(updateFeatureVote('alice', feat, false)).resolves.not.toThrow();
});
test('vyhodí chybu po 4 hlasech', async () => {
const features = ['FA', 'FB', 'FC', 'FD'] as FeatureRequest[];
for (const f of features) {
await updateFeatureVote('alice', f, true);
}
await expect(updateFeatureVote('alice', 'FE' as FeatureRequest, true)).rejects.toThrow('4');
});
});
describe('getVotingStats', () => {
test('vrátí agregované počty hlasů', async () => {
await updateFeatureVote('alice', 'FA' as FeatureRequest, true);
await updateFeatureVote('bob', 'FA' as FeatureRequest, true);
await updateFeatureVote('bob', 'FB' as FeatureRequest, true);
const stats = await getVotingStats();
expect(stats['FA']).toBe(2);
expect(stats['FB']).toBe(1);
});
test('vrátí prázdný objekt bez hlasů', async () => {
const stats = await getVotingStats();
expect(stats).toEqual({});
});
});