Files
multi-simadmin/apps/api/src/application/organization/device-organization-service.test.ts
T
chick f9186bd851 feat(api): absorb the Hub control plane into the local instance model
Add central notification channels, rules, queue and delivery logs, fleet
organization groups and tags, device discovery, the device action catalog,
instance module reads, the log centre, connection settings and system
maintenance as native /api/v1 routes backed by the existing secret store,
audit trail and pinned upstream transport.
2026-09-05 18:53:04 +08:00

119 lines
4.2 KiB
TypeScript

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');
});
});