532 lines
19 KiB
TypeScript
532 lines
19 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
// Mock logger
|
|
vi.mock('@adonisjs/core/services/logger', () => ({
|
|
default: { info: vi.fn(), debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
}))
|
|
|
|
// Mock DeviceModel
|
|
vi.mock('../../../../app/models/device.js', () => ({
|
|
default: {
|
|
createOrUpdate: vi.fn().mockResolvedValue({}),
|
|
findByDeviceId: vi.fn().mockResolvedValue(null),
|
|
updateOne: vi.fn().mockResolvedValue({}),
|
|
},
|
|
}))
|
|
|
|
import { DeviceActionExecutor } from '../../../../app/services/cwmp/device_action_executor.js'
|
|
import { CWMPTaskQueue, ResultStatus } from '../../../../app/services/cwmp/task_queue.js'
|
|
import { Device } from '../../../../app/domain/device/device.js'
|
|
import { DeviceId } from '../../../../app/domain/device/device_id.js'
|
|
import { DeviceProperty, DeviceParameter } from '../../../../app/domain/device/device_parameter.js'
|
|
import { ParameterType } from '../../../../app/domain/shared/types.js'
|
|
import DeviceModel from '../../../../app/models/device.js'
|
|
|
|
function makeDevice(dataModel: string = 'tr-181'): Device {
|
|
const device = new Device(new DeviceId('Sagemcom', 'F8B7FF', 'Livebox 4', 'LB4-TEST-001'))
|
|
device.setDataModel(dataModel as any)
|
|
return device
|
|
}
|
|
|
|
describe('DeviceActionExecutor — coverage', () => {
|
|
let taskQueue: CWMPTaskQueue
|
|
let executor: DeviceActionExecutor
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
taskQueue = new CWMPTaskQueue()
|
|
executor = new DeviceActionExecutor(taskQueue)
|
|
})
|
|
|
|
// --- getParameterValues ---
|
|
|
|
it('getParameterValues returns XML containing GetParameterValues', async () => {
|
|
const device = makeDevice()
|
|
const xml = await executor.getParameterValues(device, ['Device.Info.SW'], async () => '')
|
|
expect(xml).toContain('GetParameterValues')
|
|
expect(xml).toContain('Device.Info.SW')
|
|
})
|
|
|
|
it('getParameterValues wrappedCallback updates device on success', async () => {
|
|
const device = makeDevice()
|
|
await executor.getParameterValues(device, ['Device.Info.SW'], async () => '')
|
|
|
|
// Execute the callback directly via executeTaskCallback
|
|
const taskCallbacks = (taskQueue as any).taskCallbacks as Map<string, any>
|
|
const taskId = Array.from(taskCallbacks.keys())[0]
|
|
expect(taskId).toBeDefined()
|
|
|
|
await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {
|
|
ParameterList: {
|
|
ParameterValueStruct: [
|
|
{ Name: 'Device.Info.SW', Value: { _: '5.0.1', $: { Type: 'xsd:string' } } },
|
|
],
|
|
},
|
|
})
|
|
|
|
// Device should have been updated
|
|
const prop = device.getProperty('Device.Info.SW')
|
|
expect(prop).not.toBeNull()
|
|
expect(prop!.value).toBe('5.0.1')
|
|
})
|
|
|
|
// --- getParameterNames ---
|
|
|
|
it('getParameterNames returns GPN envelope', async () => {
|
|
const device = makeDevice()
|
|
const xml = await executor.getParameterNames(device, 'Device.ManagementServer.', async () => '')
|
|
expect(xml).toContain('GetParameterNames')
|
|
expect(xml).toContain('Device.ManagementServer.')
|
|
})
|
|
|
|
it('GPN success marks device as supported and records success', async () => {
|
|
const device = makeDevice()
|
|
device.setGpnStatus('unknown')
|
|
let capturedCallback: any
|
|
vi.spyOn(taskQueue, 'addTask').mockImplementation((_deviceId, _type, _props, cb) => {
|
|
capturedCallback = cb
|
|
return { xml: '<gpn/>', taskId: 'tid-1' }
|
|
})
|
|
|
|
await executor.getParameterNames(device, 'Device.', async () => '')
|
|
await capturedCallback({ status: ResultStatus.Success, data: { ParameterList: { ParameterInfoStruct: [] } } })
|
|
|
|
expect(device.gpnStatus).toBe('supported')
|
|
expect(device.gpnCircuitBreaker.failureCount).toBe(0)
|
|
})
|
|
|
|
it('GPN fault 9001 marks device as unsupported and records failure', async () => {
|
|
const device = makeDevice()
|
|
device.setGpnStatus('unknown')
|
|
let capturedCallback: any
|
|
vi.spyOn(taskQueue, 'addTask').mockImplementation((_deviceId, _type, _props, cb) => {
|
|
capturedCallback = cb
|
|
return { xml: '<gpn/>', taskId: 'tid-2' }
|
|
})
|
|
|
|
await executor.getParameterNames(device, 'Device.', async () => '')
|
|
await capturedCallback({ status: ResultStatus.Error, data: { FaultCode: '9001', FaultString: 'Not supported' } })
|
|
|
|
expect(device.gpnStatus).toBe('unsupported')
|
|
expect(device.gpnCircuitBreaker.failureCount).toBe(1)
|
|
})
|
|
|
|
it('GPN fault 9005 marks device as unsupported', async () => {
|
|
const device = makeDevice()
|
|
device.setGpnStatus('unknown')
|
|
let capturedCallback: any
|
|
vi.spyOn(taskQueue, 'addTask').mockImplementation((_deviceId, _type, _props, cb) => {
|
|
capturedCallback = cb
|
|
return { xml: '<gpn/>', taskId: 'tid-3' }
|
|
})
|
|
|
|
await executor.getParameterNames(device, 'Device.', async () => '')
|
|
await capturedCallback({ status: ResultStatus.Error, data: { FaultCode: 9005 } })
|
|
|
|
expect(device.gpnStatus).toBe('unsupported')
|
|
expect(device.gpnCircuitBreaker.failureCount).toBe(1)
|
|
})
|
|
|
|
it('GPN fault 9002 is treated as transient (no unsupported mark)', async () => {
|
|
const device = makeDevice()
|
|
device.setGpnStatus('unknown')
|
|
let capturedCallback: any
|
|
vi.spyOn(taskQueue, 'addTask').mockImplementation((_deviceId, _type, _props, cb) => {
|
|
capturedCallback = cb
|
|
return { xml: '<gpn/>', taskId: 'tid-4' }
|
|
})
|
|
|
|
await executor.getParameterNames(device, 'Device.', async () => '')
|
|
await capturedCallback({ status: ResultStatus.Error, data: { FaultCode: '9002' } })
|
|
|
|
// Should NOT mark as unsupported
|
|
expect(device.gpnStatus).toBe('unknown')
|
|
expect(device.gpnCircuitBreaker.failureCount).toBe(0)
|
|
})
|
|
|
|
// --- applySetParameterValues ---
|
|
|
|
it('applySetParameterValues returns SPV envelope', async () => {
|
|
const device = makeDevice()
|
|
const xml = await executor.applySetParameterValues(
|
|
device,
|
|
[{ parameterName: 'Device.X.Interval', value: '300', type: 'unsignedInt' }],
|
|
async () => ''
|
|
)
|
|
expect(xml).toContain('SetParameterValues')
|
|
expect(xml).toContain('Device.X.Interval')
|
|
expect(xml).toContain('300')
|
|
expect(xml).toContain('xsd:unsignedInt')
|
|
})
|
|
|
|
it('applySetParameterValues infers type from device param when not specified', async () => {
|
|
const device = makeDevice()
|
|
device.setParameter(new DeviceParameter('Device.X.Flag', 'true', ParameterType.BOOLEAN))
|
|
|
|
const xml = await executor.applySetParameterValues(
|
|
device,
|
|
[{ parameterName: 'Device.X.Flag', value: 'false' }],
|
|
async () => ''
|
|
)
|
|
expect(xml).toContain('xsd:boolean')
|
|
})
|
|
|
|
it('applySetParameterValues defaults to xsd:string for unknown type', async () => {
|
|
const device = makeDevice()
|
|
const xml = await executor.applySetParameterValues(
|
|
device,
|
|
[{ parameterName: 'Device.X.New', value: 'hello' }],
|
|
async () => ''
|
|
)
|
|
expect(xml).toContain('xsd:string')
|
|
})
|
|
|
|
// --- insertProvisioningCode ---
|
|
|
|
it('insertProvisioningCode uses tr-181 path for tr-181 device', async () => {
|
|
const device = makeDevice('tr-181')
|
|
device.tmpPC = 'my-prov-code'
|
|
const xml = await executor.insertProvisioningCode(device, async () => '')
|
|
expect(xml).toContain('Device.DeviceInfo.ProvisioningCode')
|
|
expect(xml).toContain('my-prov-code')
|
|
})
|
|
|
|
it('insertProvisioningCode uses tr-098 path for tr-098 device', async () => {
|
|
const device = makeDevice('tr-098')
|
|
device.tmpPC = 'code-098'
|
|
const xml = await executor.insertProvisioningCode(device, async () => '')
|
|
expect(xml).toContain('InternetGatewayDevice.DeviceInfo.ProvisioningCode')
|
|
})
|
|
|
|
it('insertProvisioningCode uses known param path when available', async () => {
|
|
const device = makeDevice('tr-181')
|
|
device.setProperty(new DeviceProperty('Device.DeviceInfo.ProvisioningCode', 'old-code', ParameterType.STRING))
|
|
device.tmpPC = 'new-code'
|
|
const xml = await executor.insertProvisioningCode(device, async () => '')
|
|
expect(xml).toContain('Device.DeviceInfo.ProvisioningCode')
|
|
expect(xml).toContain('new-code')
|
|
})
|
|
|
|
// --- persistDevice ---
|
|
|
|
it('persistDevice calls DeviceModel.createOrUpdate', async () => {
|
|
const device = makeDevice()
|
|
let capturedCallback: any
|
|
vi.spyOn(taskQueue, 'addTask').mockImplementation((_deviceId, _type, _props, cb) => {
|
|
capturedCallback = cb
|
|
return { xml: '<xml/>', taskId: 'tid-p' }
|
|
})
|
|
|
|
await executor.getParameterValues(device, ['Device.X'], async () => '')
|
|
await capturedCallback({
|
|
status: ResultStatus.Success,
|
|
data: { ParameterList: { ParameterValueStruct: [{ Name: 'Device.X', Value: { _: 'v' } }] } },
|
|
})
|
|
|
|
expect(vi.mocked(DeviceModel.createOrUpdate)).toHaveBeenCalledWith(
|
|
expect.objectContaining({ identifier: expect.any(String) })
|
|
)
|
|
})
|
|
|
|
// --- Download/Reboot ---
|
|
|
|
it('applyDownload returns Download envelope', async () => {
|
|
const device = makeDevice()
|
|
const xml = await executor.applyDownload(device, {
|
|
commandKey: 'fw-1',
|
|
fileType: '1 Firmware Upgrade Image',
|
|
url: 'http://example.com/fw.bin',
|
|
}, async () => '')
|
|
expect(xml).toContain('Download')
|
|
expect(xml).toContain('http://example.com/fw.bin')
|
|
})
|
|
|
|
it('applyReboot returns Reboot envelope', async () => {
|
|
const device = makeDevice()
|
|
const xml = await executor.applyReboot(device, { commandKey: 'reboot-1' }, async () => '')
|
|
expect(xml).toContain('Reboot')
|
|
expect(xml).toContain('reboot-1')
|
|
})
|
|
|
|
it('applyFactoryReset returns FactoryReset envelope', async () => {
|
|
const device = makeDevice()
|
|
const xml = await executor.applyFactoryReset(device, {}, async () => '')
|
|
expect(xml).toContain('FactoryReset')
|
|
})
|
|
})
|
|
|
|
describe('DeviceActionExecutor — callback execution for remaining uncovered lines', () => {
|
|
let taskQueue: CWMPTaskQueue
|
|
let executor: DeviceActionExecutor
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
taskQueue = new CWMPTaskQueue()
|
|
executor = new DeviceActionExecutor(taskQueue)
|
|
})
|
|
|
|
// --- getParameterAttributes callback (lines 118-145) ---
|
|
|
|
it('getParameterAttributes wrappedCallback processes attributes on success', async () => {
|
|
const device = makeDevice()
|
|
device.setParameter(new DeviceParameter('Device.X.Param', 'val', ParameterType.STRING))
|
|
|
|
await executor.getParameterAttributes(device, ['Device.X.Param'], async () => '')
|
|
|
|
const taskCallbacks = (taskQueue as any).taskCallbacks as Map<string, any>
|
|
const taskId = Array.from(taskCallbacks.keys())[0]
|
|
|
|
await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {
|
|
ParameterList: {
|
|
ParameterAttributeStruct: [
|
|
{ Name: 'Device.X.Param', Notification: 2, AccessList: { string: ['Subscriber'] } },
|
|
],
|
|
},
|
|
})
|
|
|
|
// Should have updated the parameter's attributes
|
|
const param = device.getParameter('Device.X.Param')
|
|
expect(param).not.toBeNull()
|
|
})
|
|
|
|
it('getParameterAttributes wrappedCallback handles property attributes', async () => {
|
|
const device = makeDevice()
|
|
device.setProperty(new DeviceProperty('Device.Y.Prop', 'pval', ParameterType.STRING))
|
|
|
|
await executor.getParameterAttributes(device, ['Device.Y.Prop'], async () => '')
|
|
|
|
const taskCallbacks = (taskQueue as any).taskCallbacks as Map<string, any>
|
|
const taskId = Array.from(taskCallbacks.keys())[0]
|
|
|
|
await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {
|
|
ParameterList: {
|
|
ParameterAttributeStruct: [
|
|
{ Name: 'Device.Y.Prop', Notification: 1 },
|
|
],
|
|
},
|
|
})
|
|
|
|
const prop = device.getProperty('Device.Y.Prop')
|
|
expect(prop).not.toBeNull()
|
|
})
|
|
|
|
it('getParameterAttributes callback skips entries without Name', async () => {
|
|
const device = makeDevice()
|
|
await executor.getParameterAttributes(device, ['Device.X'], async () => '')
|
|
|
|
const taskCallbacks = (taskQueue as any).taskCallbacks as Map<string, any>
|
|
const taskId = Array.from(taskCallbacks.keys())[0]
|
|
|
|
// Entry without Name should be skipped without error
|
|
await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {
|
|
ParameterList: { ParameterAttributeStruct: [{ Notification: 2 }] },
|
|
})
|
|
})
|
|
|
|
// --- persistDevice error path (line 81) ---
|
|
|
|
it('persistDevice logs error when createOrUpdate fails', async () => {
|
|
vi.mocked(DeviceModel.createOrUpdate).mockRejectedValueOnce(new Error('DB down'))
|
|
const device = makeDevice()
|
|
|
|
await executor.getParameterValues(device, ['Device.X'], async () => '')
|
|
const taskCallbacks = (taskQueue as any).taskCallbacks as Map<string, any>
|
|
const taskId = Array.from(taskCallbacks.keys())[0]
|
|
|
|
await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {
|
|
ParameterList: { ParameterValueStruct: [{ Name: 'Device.X', Value: { _: 'v' } }] },
|
|
})
|
|
|
|
expect(vi.mocked(logger.error)).toHaveBeenCalled()
|
|
})
|
|
|
|
// --- getParameterValues wrappedCallback error path (line 103) ---
|
|
|
|
it('getParameterValues wrappedCallback catches errors in updateFromValues', async () => {
|
|
const device = makeDevice()
|
|
// Force updateFromValues to throw
|
|
vi.spyOn(device, 'updateFromValues').mockImplementationOnce(() => { throw new Error('parse error') })
|
|
|
|
await executor.getParameterValues(device, ['Device.X'], async () => '')
|
|
const taskCallbacks = (taskQueue as any).taskCallbacks as Map<string, any>
|
|
const taskId = Array.from(taskCallbacks.keys())[0]
|
|
|
|
await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {
|
|
ParameterList: { ParameterValueStruct: [{ Name: 'Device.X', Value: { _: 'v' } }] },
|
|
})
|
|
|
|
expect(vi.mocked(logger.error)).toHaveBeenCalled()
|
|
})
|
|
|
|
// --- applySetParameterValues callback (line 247) ---
|
|
|
|
it('applySetParameterValues callback updates device on success', async () => {
|
|
const device = makeDevice()
|
|
const callbackResult = vi.fn().mockResolvedValue('')
|
|
|
|
await executor.applySetParameterValues(device, [{ parameterName: 'Device.X', value: '42', type: 'int' }], callbackResult)
|
|
|
|
const taskCallbacks = (taskQueue as any).taskCallbacks as Map<string, any>
|
|
const taskId = Array.from(taskCallbacks.keys())[0]
|
|
await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {})
|
|
|
|
expect(callbackResult).toHaveBeenCalledWith(true)
|
|
})
|
|
|
|
it('applySetParameterValues callback returns false on failure', async () => {
|
|
const device = makeDevice()
|
|
const callbackResult = vi.fn().mockResolvedValue('')
|
|
|
|
await executor.applySetParameterValues(device, [{ parameterName: 'Device.X', value: '42' }], callbackResult)
|
|
|
|
const taskCallbacks = (taskQueue as any).taskCallbacks as Map<string, any>
|
|
const taskId = Array.from(taskCallbacks.keys())[0]
|
|
await taskQueue.executeTaskCallback(taskId!, ResultStatus.Error, { FaultCode: 9001 })
|
|
|
|
expect(callbackResult).toHaveBeenCalledWith(false)
|
|
})
|
|
|
|
// --- applySetParameterAttributes callback (lines 270-273) ---
|
|
|
|
it('applySetParameterAttributes callback invoked on success', async () => {
|
|
const device = makeDevice()
|
|
const cb = vi.fn().mockResolvedValue('')
|
|
|
|
await executor.applySetParameterAttributes(device, [
|
|
{ name: 'Device.X', notificationChange: true, notification: 2, accessListChange: false, accessList: [] },
|
|
], cb)
|
|
|
|
const taskCallbacks = (taskQueue as any).taskCallbacks as Map<string, any>
|
|
const taskId = Array.from(taskCallbacks.keys())[0]
|
|
await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {})
|
|
|
|
expect(cb).toHaveBeenCalledWith(true)
|
|
})
|
|
|
|
// --- applyScheduleInform callback (line 289) ---
|
|
|
|
it('applyScheduleInform callback invoked', async () => {
|
|
const device = makeDevice()
|
|
const cb = vi.fn().mockResolvedValue('')
|
|
|
|
await executor.applyScheduleInform(device, { delayInSeconds: 600 }, cb)
|
|
|
|
const taskCallbacks = (taskQueue as any).taskCallbacks as Map<string, any>
|
|
const taskId = Array.from(taskCallbacks.keys())[0]
|
|
await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {})
|
|
|
|
expect(cb).toHaveBeenCalledWith(true)
|
|
})
|
|
|
|
// --- applyReboot callback (line 319) ---
|
|
|
|
it('applyReboot callback invoked on success', async () => {
|
|
const device = makeDevice()
|
|
const cb = vi.fn().mockResolvedValue('')
|
|
|
|
await executor.applyReboot(device, { commandKey: 'r1' }, cb)
|
|
|
|
const taskCallbacks = (taskQueue as any).taskCallbacks as Map<string, any>
|
|
const taskId = Array.from(taskCallbacks.keys())[0]
|
|
await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {})
|
|
|
|
expect(cb).toHaveBeenCalledWith(true)
|
|
})
|
|
|
|
// --- applyUpload callback (line 367) ---
|
|
|
|
it('applyUpload returns Upload envelope and callback invoked', async () => {
|
|
const device = makeDevice()
|
|
const cb = vi.fn().mockResolvedValue('')
|
|
|
|
const xml = await executor.applyUpload(device, {
|
|
commandKey: 'upload-1', fileType: '2 Web Content', url: 'http://upload.example.com/cfg',
|
|
}, cb)
|
|
|
|
expect(xml).toContain('Upload')
|
|
expect(xml).toContain('http://upload.example.com/cfg')
|
|
|
|
const taskCallbacks = (taskQueue as any).taskCallbacks as Map<string, any>
|
|
const taskId = Array.from(taskCallbacks.keys())[0]
|
|
await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {})
|
|
|
|
expect(cb).toHaveBeenCalledWith(true)
|
|
})
|
|
|
|
// --- insertProvisioningCode success callback (line 409) ---
|
|
|
|
it('insertProvisioningCode callback clears tmpPC on success', async () => {
|
|
const device = makeDevice()
|
|
device.tmpPC = 'my-prov-code'
|
|
const cb = vi.fn().mockResolvedValue('')
|
|
|
|
await executor.insertProvisioningCode(device, cb)
|
|
|
|
const taskCallbacks = (taskQueue as any).taskCallbacks as Map<string, any>
|
|
const taskId = Array.from(taskCallbacks.keys())[0]
|
|
await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {})
|
|
|
|
expect(cb).toHaveBeenCalledWith(true)
|
|
expect(device.tmpPC).toBe('')
|
|
})
|
|
|
|
it('insertProvisioningCode callback returns false on failure', async () => {
|
|
const device = makeDevice()
|
|
device.tmpPC = 'prov-code'
|
|
const cb = vi.fn().mockResolvedValue('')
|
|
|
|
await executor.insertProvisioningCode(device, cb)
|
|
|
|
const taskCallbacks = (taskQueue as any).taskCallbacks as Map<string, any>
|
|
const taskId = Array.from(taskCallbacks.keys())[0]
|
|
await taskQueue.executeTaskCallback(taskId!, ResultStatus.Error, {})
|
|
|
|
expect(cb).toHaveBeenCalledWith(false)
|
|
// tmpPC should NOT be cleared on failure
|
|
expect(device.tmpPC).toBe('prov-code')
|
|
})
|
|
|
|
// --- GPN with nested fault code path (lines 204-205) ---
|
|
|
|
it('GPN fault with nested Detail.Fault.FaultCode marks as unsupported', async () => {
|
|
const device = makeDevice()
|
|
device.setGpnStatus('unknown')
|
|
let capturedCallback: any
|
|
vi.spyOn(taskQueue, 'addTask').mockImplementation((_deviceId, _type, _props, cb) => {
|
|
capturedCallback = cb
|
|
return { xml: '<gpn/>', taskId: 'tid-nested' }
|
|
})
|
|
|
|
await executor.getParameterNames(device, 'Device.', async () => '')
|
|
await capturedCallback({
|
|
status: ResultStatus.Error,
|
|
data: { Detail: { Fault: { FaultCode: '9003' } } },
|
|
})
|
|
|
|
expect(device.gpnStatus).toBe('unsupported')
|
|
})
|
|
|
|
// --- insertProvisioningCode legacy fallback path ---
|
|
|
|
it('insertProvisioningCode uses legacy fallback when dataModel is unknown', async () => {
|
|
const device = makeDevice('unknown')
|
|
// No Device. prefixed params, so should use InternetGatewayDevice path
|
|
device.tmpPC = 'test-pc'
|
|
|
|
const xml = await executor.insertProvisioningCode(device, async () => '')
|
|
expect(xml).toContain('InternetGatewayDevice.DeviceInfo.ProvisioningCode')
|
|
})
|
|
|
|
it('insertProvisioningCode uses Device path when device has Device. params', async () => {
|
|
const device = makeDevice('unknown')
|
|
device.setProperty(new DeviceProperty('Device.DeviceInfo.X', 'val', ParameterType.STRING))
|
|
device.tmpPC = 'test-pc'
|
|
|
|
const xml = await executor.insertProvisioningCode(device, async () => '')
|
|
expect(xml).toContain('Device.DeviceInfo.ProvisioningCode')
|
|
})
|
|
})
|
|
|
|
import logger from '@adonisjs/core/services/logger'
|
|
|