- 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.
22 lines
3.0 KiB
JavaScript
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 };
|