Files
adam-acs/tests/unit/services/device_service.spec.ts
Kevin L'Hereec 76e75132d5
Build & Deploy / build (push) Successful in 34s
Build & Deploy / docker (push) Successful in 1m19s
Build & Deploy / deploy (push) Successful in 37s
test: raise business-logic unit test coverage across core services
Adds targeted unit tests to cover previously untested branches/functions
in the business-critical (non-protocol) services:

- device_service.ts: 35.8% -> 98.76% (CRUD, executeCommand, refreshDevice,
  calculatePagination, IPv4/IPv6 Connection Request fallback chain)
- domain/device/device.ts: 76.7% -> 96.59% (GPN circuit breaker state
  machine, session lifecycle, canExecuteCommands, detectDataModelFromParams)
- services/references/reference_engine.ts: 78.66% -> 100% (provider
  loading edge cases, rejection handling, singleton lifecycle)
- services/configuration_engine.ts: 70.96% -> 98.7% (matchesCriteria
  branches, shouldApplyOnce policy variants, dryRun, complianceReport)
- services/cwmp/state_machine_manager.ts: 70.9% -> 87.27% (getHashIdForRemoteIp,
  ensureSessionForRemote IP fallback, task callback error handling)
- services/cwmp/main_state_machine.ts: 57.79% -> 94.29% (ExecutingCommandsState
  full command-type dispatch, MAX_STATE_TRANSITIONS guard, error->
  PartialOperationalState transitions)

Also fixes:
- reference_engine_full.spec.ts previously depended on the real
  config/references.json (production config, wassup provider legitimately
  disabled there) instead of a test fixture — now mocks config_loader
  for deterministic, isolated assertions
- main_state_machine.spec.ts CommandModel.getCollection mock returned a
  new object per call instead of a shared instance, breaking
  setupCommandMock() for the new command-dispatch tests

Overall project coverage (excluding protocol/): 72.76% -> 82.34% statements.
Full suite: 1005/1005 tests passing.
2026-07-22 11:45:50 +02:00

720 lines
26 KiB
TypeScript

import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import DeviceService from '../../../app/services/device_service.js'
import type { DeviceRecord } from '../../../app/domain/device/device_record.js'
import { CommandStatus, CommandType, Priority } from '../../../app/domain/commands/command.js'
import { ModeType } from '../../../app/domain/shared/mode.js'
import DeviceModel from '../../../app/models/device.js'
import CommandModel from '../../../app/models/command.js'
// ── Mocks ──────────────────────────────────────────────────────────────────────
vi.mock('@adonisjs/core/services/logger', () => ({
default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
}))
vi.mock('../../../app/models/device.js', () => ({
default: {
findByIdentifier: vi.fn(),
paginateDevices: vi.fn(),
deleteOne: vi.fn(),
},
}))
vi.mock('../../../app/models/command.js', () => ({
default: {
createCommand: vi.fn(),
getCollection: vi.fn(() => ({
findOne: vi.fn().mockResolvedValue(null),
find: vi.fn(() => ({ toArray: vi.fn().mockResolvedValue([]) })),
updateOne: vi.fn(),
})),
},
}))
// ── Helpers ────────────────────────────────────────────────────────────────────
function makeDevice(overrides: Partial<DeviceRecord> = {}): DeviceRecord {
return {
identifier: 'TestMfr-AABBCC-TestBox-SN001',
deviceId: {
manufacturer: 'TestMfr',
oui: 'AABBCC',
productClass: 'TestBox',
serialNumber: 'SN001',
},
properties: [],
parameters: [],
...overrides,
} as unknown as DeviceRecord
}
function makeProperty(name: string, value: string) {
return { name, value }
}
// ── Tests ──────────────────────────────────────────────────────────────────────
describe('DeviceService — sendWakeOnWlan / Connection Request', () => {
let service: DeviceService
let fetchMock: ReturnType<typeof vi.fn>
beforeEach(() => {
service = new DeviceService()
fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock)
})
afterEach(() => {
vi.unstubAllGlobals()
vi.clearAllMocks()
})
// ── IPv4 ───────────────────────────────────────────────────────────────────
describe('IPv4 Connection Request', () => {
it('retourne true quand le CPE répond 200 sur une URL IPv4', async () => {
const device = makeDevice({
properties: [
makeProperty(
'Device.ManagementServer.ConnectionRequestURL',
'http://192.168.1.100:7547/cr'
),
makeProperty('Device.ManagementServer.Username', 'admin'),
makeProperty('Device.ManagementServer.Password', 'secret'),
],
})
fetchMock.mockResolvedValue({ ok: true, status: 200 })
const result = await (service as any).sendWakeOnWlan(
new URL('http://192.168.1.100:7547/cr'),
device
)
expect(result).toBe(true)
expect(fetchMock).toHaveBeenCalledOnce()
const [calledUrl] = fetchMock.mock.calls[0]
expect(calledUrl).toBe('http://192.168.1.100:7547/cr')
})
it('retourne false et log error quand le CPE répond 401', async () => {
const device = makeDevice({
properties: [
makeProperty('Device.ManagementServer.ConnectionRequestURL', 'http://10.0.0.1:7547/'),
],
})
fetchMock.mockResolvedValue({ ok: false, status: 401, statusText: 'Unauthorized' })
const result = await (service as any).sendWakeOnWlan(new URL('http://10.0.0.1:7547/'), device)
expect(result).toBe(false)
})
it('retourne false quand fetch lève une erreur réseau IPv4', async () => {
const device = makeDevice({
properties: [
makeProperty('Device.ManagementServer.ConnectionRequestURL', 'http://10.0.0.1:7547/'),
],
})
const networkError = Object.assign(new Error('fetch failed'), {
cause: { code: 'ECONNREFUSED' },
})
fetchMock.mockRejectedValue(networkError)
const result = await (service as any).sendWakeOnWlan(new URL('http://10.0.0.1:7547/'), device)
expect(result).toBe(false)
})
})
// ── IPv6 ───────────────────────────────────────────────────────────────────
describe('IPv6 Connection Request', () => {
it('retourne true quand le CPE répond 200 sur une URL IPv6', async () => {
const ipv6Url = 'http://[2a01:cb15:890f:4a00:56ec:b0ff:fe9c:e490]:50895/kOmisavuzLYxpksz'
const device = makeDevice({
properties: [
makeProperty('Device.ManagementServer.ConnectionRequestURL', ipv6Url),
makeProperty('Device.ManagementServer.Username', 'user'),
makeProperty('Device.ManagementServer.Password', 'pass'),
],
})
fetchMock.mockResolvedValue({ ok: true, status: 200 })
const result = await (service as any).sendWakeOnWlan(new URL(ipv6Url), device)
expect(result).toBe(true)
expect(fetchMock).toHaveBeenCalledOnce()
const [calledUrl, init] = fetchMock.mock.calls[0]
// L'URL doit conserver les crochets IPv6
expect(calledUrl).toBe(ipv6Url)
// Le header Authorization doit être présent
expect(init.headers?.Authorization).toMatch(/^Basic /)
})
it('envoie la bonne URL IPv6 complète avec path et port', async () => {
const ipv6Url = 'http://[::1]:8080/wake'
const device = makeDevice({
properties: [makeProperty('Device.ManagementServer.ConnectionRequestURL', ipv6Url)],
})
fetchMock.mockResolvedValue({ ok: true, status: 204 })
await (service as any).sendWakeOnWlan(new URL(ipv6Url), device)
expect(fetchMock.mock.calls[0][0]).toBe('http://[::1]:8080/wake')
})
it("retourne false et ENETUNREACH est loggé quand l'IPv6 n'est pas joignable", async () => {
const ipv6Url = 'http://[2a01:cb15:890f:4a00:56ec:b0ff:fe9c:e490]:50895/cr'
const device = makeDevice({
properties: [makeProperty('Device.ManagementServer.ConnectionRequestURL', ipv6Url)],
})
const networkError = Object.assign(new Error('fetch failed'), {
cause: { code: 'ENETUNREACH' },
})
fetchMock.mockRejectedValue(networkError)
const result = await (service as any).sendWakeOnWlan(new URL(ipv6Url), device)
expect(result).toBe(false)
})
it('retourne false quand le CPE IPv6 répond 403', async () => {
const ipv6Url = 'http://[fe80::1]:7547/cr'
const device = makeDevice({
properties: [makeProperty('Device.ManagementServer.ConnectionRequestURL', ipv6Url)],
})
fetchMock.mockResolvedValue({ ok: false, status: 403, statusText: 'Forbidden' })
const result = await (service as any).sendWakeOnWlan(new URL(ipv6Url), device)
expect(result).toBe(false)
})
})
// ── Credentials fallback chain ─────────────────────────────────────────────
describe('Construction des credentials (Basic Auth)', () => {
it('utilise ManagementServer.Username/Password en priorité', async () => {
const device = makeDevice({
properties: [
makeProperty('Device.ManagementServer.ConnectionRequestURL', 'http://1.2.3.4:7547/'),
makeProperty('Device.ManagementServer.Username', 'mgmtUser'),
makeProperty('Device.ManagementServer.Password', 'mgmtPass'),
makeProperty('Device.ManagementServer.ConnectionRequestUsername', 'crUser'),
makeProperty('Device.ManagementServer.ConnectionRequestPassword', 'crPass'),
],
})
fetchMock.mockResolvedValue({ ok: true, status: 200 })
await (service as any).sendWakeOnWlan(new URL('http://1.2.3.4:7547/'), device)
const authHeader: string = fetchMock.mock.calls[0][1].headers.Authorization
const decoded = Buffer.from(authHeader.replace('Basic ', ''), 'base64').toString('utf-8')
expect(decoded).toBe('mgmtUser:mgmtPass')
})
it('utilise ConnectionRequestUsername/Password si ManagementServer absent', async () => {
const device = makeDevice({
properties: [
makeProperty('Device.ManagementServer.ConnectionRequestURL', 'http://1.2.3.4:7547/'),
makeProperty('Device.ManagementServer.ConnectionRequestUsername', 'crUser'),
makeProperty('Device.ManagementServer.ConnectionRequestPassword', 'crPass'),
],
})
fetchMock.mockResolvedValue({ ok: true, status: 200 })
await (service as any).sendWakeOnWlan(new URL('http://1.2.3.4:7547/'), device)
const authHeader: string = fetchMock.mock.calls[0][1].headers.Authorization
const decoded = Buffer.from(authHeader.replace('Basic ', ''), 'base64').toString('utf-8')
expect(decoded).toBe('crUser:crPass')
})
it('utilise OUI-ProductClass-SerialNumber comme username par défaut', async () => {
const device = makeDevice({
properties: [
makeProperty('Device.ManagementServer.ConnectionRequestURL', 'http://1.2.3.4:7547/'),
],
})
fetchMock.mockResolvedValue({ ok: true, status: 200 })
await (service as any).sendWakeOnWlan(new URL('http://1.2.3.4:7547/'), device)
const authHeader: string = fetchMock.mock.calls[0][1].headers.Authorization
const decoded = Buffer.from(authHeader.replace('Basic ', ''), 'base64').toString('utf-8')
// format: OUI-ProductClass-SerialNumber:
expect(decoded).toBe('AABBCC-TestBox-SN001:')
})
it('fonctionne avec un password vide', async () => {
const device = makeDevice({
properties: [
makeProperty('Device.ManagementServer.ConnectionRequestURL', 'http://1.2.3.4:7547/'),
makeProperty('Device.ManagementServer.Username', 'admin'),
],
})
fetchMock.mockResolvedValue({ ok: true, status: 200 })
await (service as any).sendWakeOnWlan(new URL('http://1.2.3.4:7547/'), device)
const authHeader: string = fetchMock.mock.calls[0][1].headers.Authorization
const decoded = Buffer.from(authHeader.replace('Basic ', ''), 'base64').toString('utf-8')
expect(decoded).toBe('admin:')
})
})
// ── IPv4 fallback (lastRemoteIp) ─────────────────────────────────────────────
describe('Fallback IPv4 via lastRemoteIp', () => {
it('essaie le fallback IPv4 quand la première URL (IPv6) échoue', async () => {
const ipv6Url = 'http://[2a01:cb15:890f:4a00:56ec:b0ff:fe9c:e490]:7547/cr'
const device = makeDevice({
properties: [makeProperty('Device.ManagementServer.ConnectionRequestURL', ipv6Url)],
lastRemoteIp: '203.0.113.5',
} as any)
fetchMock
.mockRejectedValueOnce(
Object.assign(new Error('fetch failed'), { cause: { code: 'ENETUNREACH' } })
)
.mockResolvedValueOnce({ ok: true, status: 200 })
const result = await (service as any).sendWakeOnWlan(new URL(ipv6Url), device)
expect(result).toBe(true)
expect(fetchMock).toHaveBeenCalledTimes(2)
expect(fetchMock.mock.calls[1][0]).toBe('http://203.0.113.5:7547/cr')
})
it('formatte le fallback IPv6 entre crochets si lastRemoteIp est une IPv6', async () => {
const originalUrl = 'http://192.0.2.1:7547/cr'
const device = makeDevice({
properties: [makeProperty('Device.ManagementServer.ConnectionRequestURL', originalUrl)],
lastRemoteIp: '2001:db8::1',
} as any)
fetchMock
.mockRejectedValueOnce(new Error('fetch failed'))
.mockResolvedValueOnce({ ok: true, status: 200 })
await (service as any).sendWakeOnWlan(new URL(originalUrl), device)
expect(fetchMock.mock.calls[1][0]).toBe('http://[2001:db8::1]:7547/cr')
})
it('ne tente pas de fallback si lastRemoteIp est identique au hostname original', async () => {
const url = 'http://192.0.2.1:7547/cr'
const device = makeDevice({
properties: [makeProperty('Device.ManagementServer.ConnectionRequestURL', url)],
lastRemoteIp: '192.0.2.1',
} as any)
fetchMock.mockRejectedValue(new Error('fetch failed'))
const result = await (service as any).sendWakeOnWlan(new URL(url), device)
expect(result).toBe(false)
expect(fetchMock).toHaveBeenCalledTimes(1)
})
it('retourne false si les deux URLs (originale + fallback) échouent', async () => {
const ipv6Url = 'http://[2a01:cb15::1]:7547/cr'
const device = makeDevice({
properties: [makeProperty('Device.ManagementServer.ConnectionRequestURL', ipv6Url)],
lastRemoteIp: '203.0.113.9',
} as any)
fetchMock.mockRejectedValue(new Error('fetch failed'))
const result = await (service as any).sendWakeOnWlan(new URL(ipv6Url), device)
expect(result).toBe(false)
expect(fetchMock).toHaveBeenCalledTimes(2)
})
})
// ── applyImmediateCommandForDevice (via executeCommand mode I) ──────────────
describe('applyImmediateCommandForDevice', () => {
it('ne fait rien et log une erreur si ConnectionRequestURL est absent', async () => {
const device = makeDevice({ properties: [] })
await (service as any).applyImmediateCommandForDevice(device, device.identifier)
expect(fetchMock).not.toHaveBeenCalled()
})
it('log une erreur et ne fetch pas si ConnectionRequestURL est une URL invalide', async () => {
const device = makeDevice({
properties: [
makeProperty('Device.ManagementServer.ConnectionRequestURL', 'not-a-valid-url'),
],
})
await (service as any).applyImmediateCommandForDevice(device, device.identifier)
expect(fetchMock).not.toHaveBeenCalled()
})
it('appelle sendWakeOnWlan puis met à jour le statut des commandes en attente', async () => {
const device = makeDevice({
properties: [
makeProperty('Device.ManagementServer.ConnectionRequestURL', 'http://1.2.3.4:7547/'),
],
})
fetchMock.mockResolvedValue({ ok: true, status: 200 })
const collection = {
find: vi.fn(() => ({
toArray: vi.fn().mockResolvedValue([{ _id: 'cmd-1' }, { _id: 'cmd-2' }]),
})),
updateOne: vi.fn(),
findOne: vi.fn().mockResolvedValue(null),
}
vi.mocked(CommandModel.getCollection).mockReturnValue(collection as any)
await (service as any).applyImmediateCommandForDevice(device, device.identifier)
expect(collection.updateOne).toHaveBeenCalledTimes(2)
expect(collection.updateOne).toHaveBeenCalledWith(
{ _id: 'cmd-1' },
expect.objectContaining({
$set: expect.objectContaining({ status: CommandStatus.WAITING }),
})
)
})
it('met les commandes en PENDING (pas WAITING) si la Connection Request échoue', async () => {
const device = makeDevice({
properties: [
makeProperty('Device.ManagementServer.ConnectionRequestURL', 'http://1.2.3.4:7547/'),
],
})
fetchMock.mockRejectedValue(new Error('fetch failed'))
const collection = {
find: vi.fn(() => ({ toArray: vi.fn().mockResolvedValue([{ _id: 'cmd-1' }]) })),
updateOne: vi.fn(),
findOne: vi.fn().mockResolvedValue(null),
}
vi.mocked(CommandModel.getCollection).mockReturnValue(collection as any)
await (service as any).applyImmediateCommandForDevice(device, device.identifier)
expect(collection.updateOne).toHaveBeenCalledWith(
{ _id: 'cmd-1' },
expect.objectContaining({
$set: expect.objectContaining({ status: CommandStatus.PENDING }),
})
)
})
})
})
// ── getDevices / getDeviceByIdentifier / deleteByIdentifier ───────────────────
describe('DeviceService — CRUD & pagination', () => {
let service: DeviceService
beforeEach(() => {
service = new DeviceService()
vi.clearAllMocks()
})
it('getDevices retourne data, nbDocuments et pager', async () => {
const devices = [makeDevice(), makeDevice({ identifier: 'device-2' })]
vi.mocked(DeviceModel.paginateDevices).mockResolvedValue({ data: devices, total: 50 })
const result = await service.getDevices(0, 25)
expect(DeviceModel.paginateDevices).toHaveBeenCalledWith(0, 25)
expect(result.data).toEqual(devices)
expect(result.nbDocuments).toBe(50)
expect(result.pager).toBeDefined()
})
it('getDevices utilise les valeurs par défaut offset=0, limit=100', async () => {
vi.mocked(DeviceModel.paginateDevices).mockResolvedValue({ data: [], total: 0 })
await service.getDevices()
expect(DeviceModel.paginateDevices).toHaveBeenCalledWith(0, 100)
})
it('getDeviceByIdentifier délègue à DeviceModel.findByIdentifier', async () => {
const device = makeDevice()
vi.mocked(DeviceModel.findByIdentifier).mockResolvedValue(device)
const result = await service.getDeviceByIdentifier('some-id')
expect(DeviceModel.findByIdentifier).toHaveBeenCalledWith('some-id')
expect(result).toEqual(device)
})
it('getDeviceByIdentifier retourne null si le device est introuvable', async () => {
vi.mocked(DeviceModel.findByIdentifier).mockResolvedValue(null)
const result = await service.getDeviceByIdentifier('unknown-id')
expect(result).toBeNull()
})
it('deleteByIdentifier délègue à DeviceModel.deleteOne', async () => {
vi.mocked(DeviceModel.deleteOne).mockResolvedValue(true)
const result = await service.deleteByIdentifier('some-id')
expect(DeviceModel.deleteOne).toHaveBeenCalledWith({ identifier: 'some-id' })
expect(result).toBe(true)
})
it("deleteByIdentifier retourne false si rien n'a été supprimé", async () => {
vi.mocked(DeviceModel.deleteOne).mockResolvedValue(false)
const result = await service.deleteByIdentifier('unknown-id')
expect(result).toBe(false)
})
})
describe('DeviceService.calculatePagination', () => {
let service: DeviceService
beforeEach(() => {
service = new DeviceService()
})
it('offset=0 avec total >= limit: pas de "previous", "last" et "next" présents', () => {
const pager = service.calculatePagination(0, 10, 50)
expect(pager.first).toEqual({ offset: 0, limit: 10 })
expect(pager.previous).toBeUndefined()
expect(pager.next).toEqual({ offset: 10, limit: 10 })
expect(pager.last).toEqual({ offset: 40, limit: 10 })
})
it('offset > 0: "previous" est calculé', () => {
const pager = service.calculatePagination(20, 10, 50)
expect(pager.previous).toEqual({ offset: 10, limit: 10 })
})
it('previous ne descend jamais sous 0 (Math.max)', () => {
const pager = service.calculatePagination(5, 10, 50)
expect(pager.previous).toEqual({ offset: 0, limit: 10 })
})
it('dernière page: pas de "next" quand offset + limit >= total', () => {
const pager = service.calculatePagination(40, 10, 50)
expect(pager.next).toBeUndefined()
})
it('total < limit: "last" pointe sur offset 0 (une seule page)', () => {
const pager = service.calculatePagination(0, 100, 5)
expect(pager.last).toEqual({ offset: 0, limit: 100 })
expect(pager.next).toBeUndefined()
})
it('total = 0: aucune page suivante, last à offset 0', () => {
const pager = service.calculatePagination(0, 10, 0)
expect(pager.last).toEqual({ offset: 0, limit: 10 })
expect(pager.previous).toBeUndefined()
expect(pager.next).toBeUndefined()
})
})
describe('DeviceService.executeCommand', () => {
let service: DeviceService
let fetchMock: ReturnType<typeof vi.fn>
beforeEach(() => {
service = new DeviceService()
fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock)
vi.clearAllMocks()
})
afterEach(() => {
vi.unstubAllGlobals()
})
it('lève une erreur si le device est introuvable', async () => {
vi.mocked(DeviceModel.findByIdentifier).mockResolvedValue(null)
await expect(
service.executeCommand('unknown-id', { action: CommandType.REBOOT })
).rejects.toThrow('Device not found: unknown-id')
})
it('crée une commande en mode NI par défaut (pas de Connection Request)', async () => {
const device = makeDevice()
vi.mocked(DeviceModel.findByIdentifier).mockResolvedValue(device)
const collection = {
findOne: vi.fn().mockResolvedValue(null),
find: vi.fn(() => ({ toArray: vi.fn().mockResolvedValue([]) })),
updateOne: vi.fn(),
}
vi.mocked(CommandModel.getCollection).mockReturnValue(collection as any)
await service.executeCommand(device.identifier, { action: CommandType.REBOOT })
expect(CommandModel.createCommand).toHaveBeenCalledWith(
expect.objectContaining({
deviceIdentifier: device.identifier,
action: CommandType.REBOOT,
mode: ModeType.NI,
status: CommandStatus.PENDING,
priority: Priority.NORMAL,
})
)
expect(fetchMock).not.toHaveBeenCalled()
})
it('mode "I" (insensible à la casse) déclenche une Connection Request', async () => {
const device = makeDevice({
properties: [
{ name: 'Device.ManagementServer.ConnectionRequestURL', value: 'http://1.2.3.4:7547/' },
],
})
vi.mocked(DeviceModel.findByIdentifier).mockResolvedValue(device)
const collection = {
findOne: vi.fn().mockResolvedValue(null),
find: vi.fn(() => ({ toArray: vi.fn().mockResolvedValue([]) })),
updateOne: vi.fn(),
}
vi.mocked(CommandModel.getCollection).mockReturnValue(collection as any)
fetchMock.mockResolvedValue({ ok: true, status: 200 })
await service.executeCommand(device.identifier, { action: CommandType.REBOOT, mode: 'i' })
expect(CommandModel.createCommand).toHaveBeenCalledWith(
expect.objectContaining({ mode: ModeType.I })
)
expect(fetchMock).toHaveBeenCalledOnce()
})
it('lève une erreur si une commande identique est déjà PENDING (duplicate)', async () => {
const device = makeDevice()
vi.mocked(DeviceModel.findByIdentifier).mockResolvedValue(device)
const collection = {
findOne: vi.fn().mockResolvedValue({ _id: 'existing-cmd' }),
find: vi.fn(() => ({ toArray: vi.fn().mockResolvedValue([]) })),
updateOne: vi.fn(),
}
vi.mocked(CommandModel.getCollection).mockReturnValue(collection as any)
await expect(
service.executeCommand(device.identifier, { action: CommandType.REBOOT })
).rejects.toThrow(/Duplicate pending command/)
expect(CommandModel.createCommand).not.toHaveBeenCalled()
})
it('utilise la priorité fournie plutôt que la valeur par défaut', async () => {
const device = makeDevice()
vi.mocked(DeviceModel.findByIdentifier).mockResolvedValue(device)
const collection = {
findOne: vi.fn().mockResolvedValue(null),
find: vi.fn(() => ({ toArray: vi.fn().mockResolvedValue([]) })),
updateOne: vi.fn(),
}
vi.mocked(CommandModel.getCollection).mockReturnValue(collection as any)
await service.executeCommand(device.identifier, {
action: CommandType.SET_PARAMETER_VALUES,
priority: Priority.HIGH,
})
expect(CommandModel.createCommand).toHaveBeenCalledWith(
expect.objectContaining({ priority: Priority.HIGH })
)
})
it('transmet les parameters fournis, ou {} par défaut', async () => {
const device = makeDevice()
vi.mocked(DeviceModel.findByIdentifier).mockResolvedValue(device)
const collection = {
findOne: vi.fn().mockResolvedValue(null),
find: vi.fn(() => ({ toArray: vi.fn().mockResolvedValue([]) })),
updateOne: vi.fn(),
}
vi.mocked(CommandModel.getCollection).mockReturnValue(collection as any)
await service.executeCommand(device.identifier, {
action: CommandType.SET_PARAMETER_VALUES,
parameters: { 'Device.WiFi.SSID': 'MyWifi' },
})
expect(CommandModel.createCommand).toHaveBeenCalledWith(
expect.objectContaining({ parameters: { 'Device.WiFi.SSID': 'MyWifi' } })
)
})
})
describe('DeviceService.refreshDevice', () => {
let service: DeviceService
let fetchMock: ReturnType<typeof vi.fn>
beforeEach(() => {
service = new DeviceService()
fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock)
vi.clearAllMocks()
})
afterEach(() => {
vi.unstubAllGlobals()
})
it('lève une erreur si le device est introuvable', async () => {
vi.mocked(DeviceModel.findByIdentifier).mockResolvedValue(null)
await expect(service.refreshDevice('unknown-id')).rejects.toThrow(
'Device not found: unknown-id'
)
})
it('crée une commande REFRESH en mode I et déclenche la Connection Request', async () => {
const device = makeDevice({
properties: [
{ name: 'Device.ManagementServer.ConnectionRequestURL', value: 'http://1.2.3.4:7547/' },
],
})
vi.mocked(DeviceModel.findByIdentifier).mockResolvedValue(device)
const collection = {
findOne: vi.fn().mockResolvedValue(null),
find: vi.fn(() => ({ toArray: vi.fn().mockResolvedValue([]) })),
updateOne: vi.fn(),
}
vi.mocked(CommandModel.getCollection).mockReturnValue(collection as any)
fetchMock.mockResolvedValue({ ok: true, status: 200 })
await service.refreshDevice(device.identifier)
expect(CommandModel.createCommand).toHaveBeenCalledWith(
expect.objectContaining({
action: CommandType.REFRESH,
mode: ModeType.I,
status: CommandStatus.PENDING,
priority: Priority.NORMAL,
})
)
expect(fetchMock).toHaveBeenCalledOnce()
})
})