Struktura podle zasad: rozdeleni souboru, lint, testy, prisny TypeScript

Projekt srovnan se zasadami v D:\GitHubRepository\CLAUDE.md bez zmeny chovani.

Struktura: scripts/ (skripty konektoru) -> connectors/, src/scripts ->
src/runtime/scripts; src/index.ts jen startuje, novy src/app.ts s createApp();
routes/dashboard.ts a routes/settings.ts rozdeleny do slozek; openapi.ts
rozdelen na openapi/{index,helpers,components} a paths/* (98 cest overeno
shodnych); ticketStore, automationStore a services jsou fasady nad slozkami
data/tickets, data/automations a data/services/catalog. process.env se cte
jen v config.ts. Web: hooky v hooks/, sdilena ui/Table a ui/ServiceIcon,
surove inputy nahrazeny komponentami, sedm velkych souboru rozdeleno.

Nastroje: eslint (typescript-eslint, react-hooks v7), prettier, editorconfig,
nvmrc, .env.example, vitest; skripty lint, format, test. Lint je cisty bez
jedineho eslint-disable (nove hooky useLatest a useSyncFromSource, odvozeny
stav misto setState v effectu). noUncheckedIndexedAccess v obou tsconfig,
84 mist zuzeno bez non-null operatoru; odhalilo zalohu backoffu fronty pri
nule pokusu a Retry-After NaN pri max 0. Cely kod naformatovan prettierem.

Testy: 8 souboru, 105 testu (prava, viditelnost, podminky a opakovani
v executoru, redaktor tajemstvi, sitove guardy, migrace resitelu, tickety,
health a prihlaseni pres supertest). Testy odhalily dve chyby ve vyhodnoceni
podminek, obe opravene: chybejici castka se porovnavala jako nula a podminka
nad vystupem druheho kroku cetla hodnotu prvniho se stejnym nazvem.

Pojmenovane konstanty misto magickych hodnot, ctx.util.base64 pro skripty
konektoru, README a dokumentace aktualizovany vcetne znamych odchylek.
This commit is contained in:
JiriUhlir
2026-09-09 15:11:02 +02:00
parent 42b3da8303
commit 22dda2d139
312 changed files with 21085 additions and 15319 deletions
+110
View File
@@ -0,0 +1,110 @@
/**
* Aplikace pres HTTP (src/app.ts): health, prihlaseni a prava.
*
* Data se nacitaji stejnym bootstrapem jako v `src/index.ts`, jen v pameti
* (`DATA_DIR` prazdny, viz tests/setup.ts). Zadny port se neotevira,
* supertest vola aplikaci primo.
*/
import request from 'supertest';
import { beforeAll, describe, expect, test } from 'vitest';
import type { Express } from 'express';
import { createApp } from '../../src/app.js';
import { bootstrapData } from '../../src/data/bootstrap.js';
let app: Express;
beforeAll(async () => {
const mode = await bootstrapData({ databaseReady: false });
expect(mode).toBe('memory');
app = createApp();
});
async function login(email: string, password: string): Promise<request.Response> {
return request(app).post('/api/auth/login').send({ email, password });
}
describe('health', () => {
test('GET /health odpovi 200 bez databaze', async () => {
const response = await request(app).get('/health');
expect(response.status).toBe(200);
expect(response.body.status).toBe('ok');
expect(typeof response.body.uptimeSec).toBe('number');
});
test('GET /health/ready hlasi uloziste v pameti', async () => {
const response = await request(app).get('/health/ready');
expect(response.status).toBe(200);
expect(response.body.database.enabled).toBe(false);
});
test('neznama API cesta vraci JSON 404', async () => {
const response = await request(app).get('/api/neexistuje');
expect(response.status).toBe(404);
expect(response.body.error).toBe('not_found');
});
});
describe('prihlaseni a prava', () => {
test('bez tokenu je dashboard 401', async () => {
const response = await request(app).get('/api/dashboard/access');
expect(response.status).toBe(401);
expect(response.body.error).toBe('unauthorized');
});
test('neplatny token je 401', async () => {
const response = await request(app)
.get('/api/dashboard/access')
.set('Authorization', 'Bearer neplatny');
expect(response.status).toBe(401);
});
test('spatne heslo i neznamy e-mail daji stejnou odpoved', async () => {
const wrong = await login('admin@automia.cz', 'spatne');
const unknown = await login('nikdo@automia.cz', 'demo1234');
expect(wrong.status).toBe(401);
expect(unknown.status).toBe(401);
expect(wrong.body).toEqual(unknown.body);
});
test('po prihlaseni vraci /access role a pohledy', async () => {
const loginResponse = await login('admin@automia.cz', 'demo1234');
expect(loginResponse.status).toBe(200);
expect(typeof loginResponse.body.token).toBe('string');
// Hash hesla nikdy neopousti server.
expect(loginResponse.body.user.passwordHash).toBeUndefined();
const response = await request(app)
.get('/api/dashboard/access')
.set('Authorization', `Bearer ${loginResponse.body.token}`);
expect(response.status).toBe(200);
expect(response.body.platformAdmin).toBe(true);
expect(response.body.roleNames).toEqual(['Správce firmy']);
expect(response.body.scopes).toEqual(['all', 'tenant', 'mine']);
expect(response.body.defaultTenantId).toBe('tnt_automia');
expect(response.body.permissions).toContain('tenant.manage');
});
test('clen dvou firem dostane prava za firmu z query', async () => {
const token = (await login('karel.vomacka@automia.cz', 'demo1234')).body.token as string;
const automia = await request(app)
.get('/api/dashboard/access')
.set('Authorization', `Bearer ${token}`);
expect(automia.status).toBe(200);
expect(automia.body.roleNames).toEqual(['Řešitel']);
expect(automia.body.permissions).not.toContain('user.manage');
const nordis = await request(app)
.get('/api/dashboard/access?tenantId=tnt_nordis')
.set('Authorization', `Bearer ${token}`);
expect(nordis.body.roleNames).toEqual(['Správce firmy']);
expect(nordis.body.permissions).toContain('user.manage');
// Cizi firma: prava se spocitaji za ni, ale clenstvi nesedi, takze nic.
const foreign = await request(app)
.get('/api/dashboard/tickets?tenantId=tnt_logitrans')
.set('Authorization', `Bearer ${token}`);
expect(foreign.status).toBe(404);
});
});