- Ajout de fetchWithCsrf() helper qui lit le cookie XSRF-TOKEN et injecte X-XSRF-TOKEN sur toutes les requetes POST/PUT/PATCH/DELETE vers /internal/* - Remplacement de fetch() par fetchWithCsrf() dans tous les composants Vue concernes (Tr069Trace, DeviceDetail, SequenceViewer, Home, configurations, firmware_catalog) - Activation IPv6 sur les reseaux Docker (enable_ipv6: true, subnet fd00:adam::/64) dans docker-compose.yml et docker-compose.prod.yml - Ajout tests unitaires DeviceService.sendWakeOnWlan : IPv4, IPv6, ENETUNREACH, credentials fallback chain (11 tests)
265 lines
10 KiB
TypeScript
265 lines
10 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'
|
|
|
|
// ── 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:')
|
|
})
|
|
})
|
|
})
|