Files
ridebus-admin/backend/test/helpers/boardDeviceMemoryRepository.js
Zakhar Timoshenko 360b684019 feat: implement board device service with CRUD operations and validation
- Add boardDeviceService for managing device pairing, status, claiming, and revocation.
- Introduce boardDeviceSettings for default settings and validation logic.
- Create integration tests for device lifecycle, concurrent claims, and updates.
- Add unit tests for device pairing, claiming, and settings validation.
- Implement API tests for device endpoints with JWT authentication.
- Develop in-memory repository for testing purposes.
- Create frontend components for managing devices, including a form for device settings.
- Style DevicesPage with CSS for better user experience.
2026-10-10 00:47:26 +03:00

22 lines
3.0 KiB
JavaScript

// Explicit test-only repository; production always uses PostgreSQL.
function memoryRepository({ now = Date.now } = {}) {
const pairings = new Map(), devices = new Map();
const stops = [{ id: 1, city_id: 1, name: 'Швейная фабрика', description: 'на Богатырскую' }, { id: 2, city_id: 1, name: 'Автовокзал', description: 'в центр' }, { id: 3, city_id: 2, name: 'Новополоцк', description: 'конечная' }];
const routes = [{ id: 1, city_id: 1, route_number: '1', name: 'Швейная фабрика — Богатырская', stops: [1, 2] }, { id: 2, city_id: 2, route_number: '1', name: 'Новополоцк', stops: [3] }];
const join = row => ({ ...row, city_name: row.city_id === 1 ? 'Полоцк' : 'Новополоцк', stop_name: stops.find(s => s.id === row.stop_id)?.name, stop_direction: stops.find(s => s.id === row.stop_id)?.description });
return {
stops, routesData: routes, devices, pairings,
async createPairing(p) { for (const [code, item] of pairings) if (Date.parse(item.expires_at) <= now()) pairings.delete(code); if (pairings.has(p.code)) { const e = new Error('collision'); e.code = '23505'; throw e; } pairings.set(p.code, p); return p; },
async pairing(hash) { return [...pairings.values()].find(p => p.token_hash === hash); },
async device(hash) { const row = [...devices.values()].find(d => d.token_hash === hash); return row && join(row); },
async routes(cityId, stopId) { return routes.filter(r => r.city_id === cityId && r.stops.includes(stopId)); },
async catalog(cityId, s) { return stops.some(stop => stop.id === s.stopId && stop.city_id === cityId) && (!s.routeFilter || routes.some(r => r.id === Number(s.routeFilter) && r.city_id === cityId && r.stops.includes(s.stopId))); },
async claim(code, record) { const p = pairings.get(code); if (!p || Date.parse(p.expires_at) <= now()) return null; pairings.delete(code); const row = { ...record, token_hash: p.token_hash, device_info: p.device_info, revision: 1, applied_revision: 0, last_seen_at: null, revoked_at: null }; devices.set(row.id, row); return row; },
async list(cityId) { return [...devices.values()].filter(d => d.city_id === cityId).map(join); },
async update(id, cityId, input) { const d = devices.get(id); if (!d || d.city_id !== cityId || d.revision !== input.revision || d.revoked_at) return null; Object.assign(d, { name: input.name, stop_id: input.settings.stopId, settings: input.settings, revision: d.revision + 1 }); return d; },
async revoke(id, cityId) { const d = devices.get(id); if (!d || d.city_id !== cityId) return null; d.revoked_at ||= new Date(now()).toISOString(); return d; },
async heartbeat(hash, revision, info) { const d = [...devices.values()].find(d => d.token_hash === hash && !d.revoked_at); if (!d) return null; d.applied_revision = Math.max(d.applied_revision, Math.min(d.revision, revision)); d.last_seen_at = new Date(now()).toISOString(); d.device_info = info; return d; },
};
}
module.exports = { memoryRepository };