import Database from 'better-sqlite3'; import { describe, expect, it } from 'vitest'; import { migrateDatabase } from '../../infrastructure/database/migrations.js'; import { DeviceOrganizationError, DeviceOrganizationService, } from './device-organization-service.js'; function fixture(): { db: Database.Database; service: DeviceOrganizationService } { const db = new Database(':memory:'); db.pragma('foreign_keys=ON'); migrateDatabase(db); let counter = 0; const service = new DeviceOrganizationService({ db, idFactory: () => `group-${++counter}`, now: () => new Date('2026-09-03T10:00:00.000Z'), }); return { db, service }; } function codeOf(operation: () => unknown): string { try { operation(); } catch (error) { return error instanceof DeviceOrganizationError ? error.code : 'UNEXPECTED'; } return 'NO_ERROR'; } function insertInstance(db: Database.Database, id: string, name: string, groupId: string | null) { db.prepare( `INSERT INTO instances (id,name,base_url,auth_mode,enabled,config_revision,created_at,updated_at,group_id) VALUES (?,?,?,'password',1,1,'2026-09-03T09:00:00.000Z','2026-09-03T09:00:00.000Z',?)`, ).run(id, name, `http://${id}:8080`, groupId); } describe('DeviceOrganizationService', () => { it('creates, renames, and counts group members', () => { const { db, service } = fixture(); const created = service.createGroup({ name: '总部', description: '办公区' }); expect(created).toEqual({ id: 'group-1', name: '总部', description: '办公区', deviceCount: 0, createdAt: '2026-09-03T10:00:00.000Z', updatedAt: '2026-09-03T10:00:00.000Z', }); insertInstance(db, 'i-1', 'Alpha', 'group-1'); expect(service.listGroups()[0]?.deviceCount).toBe(1); expect(service.updateGroup('group-1', { name: '上海总部' }).name).toBe('上海总部'); expect(codeOf(() => service.createGroup({ name: '上海总部' }))).toBe('DUPLICATE_GROUP'); service.deleteGroup('group-1'); expect(service.listGroups()).toEqual([]); expect( ( db.prepare('SELECT group_id FROM instances WHERE id=?').get('i-1') as { group_id: string | null; } ).group_id, ).toBeNull(); }); it('reports missing groups instead of silently succeeding', () => { const { service } = fixture(); expect(codeOf(() => service.updateGroup('nope', { name: 'x' }))).toBe('GROUP_NOT_FOUND'); expect(codeOf(() => service.deleteGroup('nope'))).toBe('GROUP_NOT_FOUND'); }); it('synchronizes the tag registry with device tags and keeps unused entries', () => { const { db, service } = fixture(); insertInstance(db, 'i-1', 'Alpha', null); db.prepare('INSERT INTO instance_tags (instance_id,tag,created_at) VALUES (?,?,?)').run( 'i-1', 'office', '2026-09-03T09:30:00.000Z', ); service.synchronizeTags(); expect(service.listTags()).toEqual([ { tag: 'office', color: '', deviceCount: 1, createdAt: '2026-09-03T09:30:00.000Z', updatedAt: '2026-09-03T10:00:00.000Z', }, ]); const tagged = service.createTag({ tag: 'spare', color: 'coral' }); expect(tagged.color).toBe('coral'); service.synchronizeTags(); expect(service.listTags().map((tag) => tag.tag)).toEqual(['office', 'spare']); }); it('detaches a deleted tag from every device', () => { const { db, service } = fixture(); insertInstance(db, 'i-1', 'Alpha', null); db.prepare('INSERT INTO instance_tags (instance_id,tag,created_at) VALUES (?,?,?)').run( 'i-1', 'lab', '2026-09-03T09:30:00.000Z', ); service.synchronizeTags(); service.deleteTag('lab'); expect(service.listTags()).toEqual([]); expect(db.prepare('SELECT COUNT(*) count FROM instance_tags').get()).toEqual({ count: 0 }); expect(codeOf(() => service.deleteTag('lab'))).toBe('TAG_NOT_FOUND'); }); it('rejects blank and oversized names', () => { const { service } = fixture(); expect(codeOf(() => service.createGroup({ name: ' ' }))).toBe('VALIDATION_FAILED'); expect(codeOf(() => service.createGroup({ name: 'x'.repeat(101) }))).toBe('VALIDATION_FAILED'); expect(codeOf(() => service.createTag({ tag: '' }))).toBe('VALIDATION_FAILED'); }); });