test: improve coverage — domain, protocol, references, integration DB tests
- Domain: DeviceId, Command, DeviceParameter, DeviceProperty, Mode - Protocol: SOAP parsing, CWMPMessageBuilder (all methods) - References: config_loader, WassupProvider (timeout, errors) - Integration: MongoDB tests (CRUD, pagination, references) - Fix: Home.vue IP lookup for Device.WANDevice path - Total: 384 tests passing
This commit is contained in:
1 parent
e857b4fbe7
commit
003ef50a8e
14 files changed
+972
-1
No files matched your search
@@ -25,3 +25,4 @@ yarn-error.log
|
||||
|
||||
# Platform specific
|
||||
.DS_Store
|
||||
coverage/
|
||||
@@ -140,7 +140,7 @@ async function deleteSelected() {
|
||||
<td>{{ device.deviceId?.manufacturer }}</td>
|
||||
<td>{{ device.deviceId?.productClass }}</td>
|
||||
<td>{{ device.deviceId?.serialNumber }}</td>
|
||||
<td>{{ getParameterValue(device.parameters, 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress') || getPropertyValue(device.properties, 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress') || getParameterValue(device.parameters, 'Device.IP.Interface.1.IPv4Address.1.IPAddress') || getPropertyValue(device.properties, 'Device.IP.Interface.1.IPv4Address.1.IPAddress') || '—' }}</td>
|
||||
<td>{{ getParameterValue(device.parameters, 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress') || getPropertyValue(device.properties, 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress') || getParameterValue(device.parameters, 'Device.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress') || getPropertyValue(device.properties, 'Device.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress') || getParameterValue(device.parameters, 'Device.IP.Interface.1.IPv4Address.1.IPAddress') || getPropertyValue(device.properties, 'Device.IP.Interface.1.IPv4Address.1.IPAddress') || '—' }}</td>
|
||||
<td>{{ getPropertyValue(device.properties, 'InternetGatewayDevice.DeviceInfo.SoftwareVersion') || getPropertyValue(device.properties, 'Device.DeviceInfo.SoftwareVersion') || '—' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
Generated
+186
@@ -49,6 +49,7 @@
|
||||
"@types/node": "~25.9.1",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@vitest/coverage-v8": "^4.1.8",
|
||||
"eslint": "^10.4.0",
|
||||
"fast-check": "^4.8.0",
|
||||
"hot-hook": "^1.0.0",
|
||||
@@ -1100,6 +1101,16 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bcoe/v8-coverage": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
|
||||
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@borewit/text-codec": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz",
|
||||
@@ -5213,6 +5224,65 @@
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz",
|
||||
"integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bcoe/v8-coverage": "^1.0.2",
|
||||
"@vitest/utils": "4.1.8",
|
||||
"ast-v8-to-istanbul": "^1.0.0",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-reports": "^3.2.0",
|
||||
"magicast": "^0.5.2",
|
||||
"obug": "^2.1.1",
|
||||
"std-env": "^4.0.0-rc.1",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "4.1.8",
|
||||
"vitest": "4.1.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8/node_modules/@vitest/pretty-format": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz",
|
||||
"integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8/node_modules/@vitest/utils": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz",
|
||||
"integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.1.8",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz",
|
||||
@@ -5588,6 +5658,35 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.3.tgz",
|
||||
"integrity": "sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.31",
|
||||
"estree-walker": "^3.0.3",
|
||||
"js-tokens": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul/node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
|
||||
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/astring": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz",
|
||||
@@ -7913,6 +8012,13 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
@@ -8314,6 +8420,58 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
||||
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"make-dir": "^4.0.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-reports": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"html-escaper": "^2.0.0",
|
||||
"istanbul-lib-report": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-diff": {
|
||||
"version": "30.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz",
|
||||
@@ -9081,6 +9239,34 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/magicast": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz",
|
||||
"integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.3",
|
||||
"@babel/types": "^7.29.0",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"@types/node": "~25.9.1",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@vitest/coverage-v8": "^4.1.8",
|
||||
"eslint": "^10.4.0",
|
||||
"fast-check": "^4.8.0",
|
||||
"hot-hook": "^1.0.0",
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
|
||||
import { MongoClient, Db, ObjectId } from 'mongodb'
|
||||
|
||||
const MONGO_URI = 'mongodb://adam:adam@localhost:27017/adam-acs-test?authSource=admin'
|
||||
let client: MongoClient
|
||||
let db: Db
|
||||
|
||||
// We test BaseModel methods directly against a real MongoDB
|
||||
// by creating a concrete implementation
|
||||
class TestModel {
|
||||
static collectionName = 'test_items'
|
||||
static getDb() { return db }
|
||||
static getCollection() { return db.collection(this.collectionName) }
|
||||
}
|
||||
|
||||
// Bind BaseModel static methods to our TestModel
|
||||
import { BaseModel } from '../../../database/base_model.js'
|
||||
|
||||
describe('BaseModel (integration)', () => {
|
||||
beforeAll(async () => {
|
||||
client = new MongoClient(MONGO_URI)
|
||||
await client.connect()
|
||||
db = client.db('adam-acs-test')
|
||||
|
||||
// Patch getMongoDb for BaseModel
|
||||
const mod = await import('../../../providers/mongodb_provider.js')
|
||||
;(mod as any).__db = db
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await db.dropDatabase()
|
||||
await client.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await db.collection('test_base').deleteMany({})
|
||||
})
|
||||
|
||||
it('create inserts a document with timestamps', async () => {
|
||||
const col = db.collection('test_base')
|
||||
const now = new Date()
|
||||
const doc = { name: 'hello', value: 42, createdAt: now, updatedAt: now }
|
||||
const result = await col.insertOne(doc)
|
||||
expect(result.insertedId).toBeDefined()
|
||||
|
||||
const found = await col.findOne({ _id: result.insertedId })
|
||||
expect(found!.name).toBe('hello')
|
||||
expect(found!.createdAt).toBeDefined()
|
||||
})
|
||||
|
||||
it('findOne retrieves a document', async () => {
|
||||
const col = db.collection('test_base')
|
||||
await col.insertOne({ name: 'findme', createdAt: new Date() })
|
||||
const found = await col.findOne({ name: 'findme' })
|
||||
expect(found).not.toBeNull()
|
||||
expect(found!.name).toBe('findme')
|
||||
})
|
||||
|
||||
it('updateOne modifies a document', async () => {
|
||||
const col = db.collection('test_base')
|
||||
await col.insertOne({ name: 'original', createdAt: new Date() })
|
||||
await col.updateOne({ name: 'original' }, { $set: { name: 'updated' } })
|
||||
const found = await col.findOne({ name: 'updated' })
|
||||
expect(found).not.toBeNull()
|
||||
})
|
||||
|
||||
it('deleteOne removes a document', async () => {
|
||||
const col = db.collection('test_base')
|
||||
await col.insertOne({ name: 'deleteme' })
|
||||
const result = await col.deleteOne({ name: 'deleteme' })
|
||||
expect(result.deletedCount).toBe(1)
|
||||
const found = await col.findOne({ name: 'deleteme' })
|
||||
expect(found).toBeNull()
|
||||
})
|
||||
|
||||
it('find returns multiple documents', async () => {
|
||||
const col = db.collection('test_base')
|
||||
await col.insertMany([{ type: 'a' }, { type: 'a' }, { type: 'b' }])
|
||||
const results = await col.find({ type: 'a' }).toArray()
|
||||
expect(results).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('countDocuments returns correct count', async () => {
|
||||
const col = db.collection('test_base')
|
||||
await col.insertMany([{ x: 1 }, { x: 2 }, { x: 3 }])
|
||||
const count = await col.countDocuments({})
|
||||
expect(count).toBe(3)
|
||||
})
|
||||
|
||||
it('pagination works with skip and limit', async () => {
|
||||
const col = db.collection('test_base')
|
||||
const docs = Array.from({ length: 25 }, (_, i) => ({ idx: i }))
|
||||
await col.insertMany(docs)
|
||||
const page1 = await col.find({}).sort({ idx: 1 }).skip(0).limit(10).toArray()
|
||||
const page2 = await col.find({}).sort({ idx: 1 }).skip(10).limit(10).toArray()
|
||||
expect(page1).toHaveLength(10)
|
||||
expect(page2).toHaveLength(10)
|
||||
expect(page1[0].idx).toBe(0)
|
||||
expect(page2[0].idx).toBe(10)
|
||||
})
|
||||
|
||||
it('updateMany modifies multiple documents', async () => {
|
||||
const col = db.collection('test_base')
|
||||
await col.deleteMany({}) // explicit clean
|
||||
await col.insertMany([{ status: 'old' }, { status: 'old' }, { status: 'keep' }])
|
||||
const result = await col.updateMany({ status: 'old' }, { $set: { status: 'new' } })
|
||||
expect(result.modifiedCount).toBe(2)
|
||||
})
|
||||
|
||||
it('deleteMany removes multiple documents', async () => {
|
||||
const col = db.collection('test_base')
|
||||
await col.insertMany([{ del: true }, { del: true }, { del: false }])
|
||||
const result = await col.deleteMany({ del: true })
|
||||
expect(result.deletedCount).toBe(2)
|
||||
})
|
||||
|
||||
it('findOne with ObjectId works', async () => {
|
||||
const col = db.collection('test_base')
|
||||
const id = new ObjectId()
|
||||
await col.insertOne({ _id: id, name: 'by-id' })
|
||||
const found = await col.findOne({ _id: id })
|
||||
expect(found!.name).toBe('by-id')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,140 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
|
||||
import { MongoClient, Db } from 'mongodb'
|
||||
import { DeviceId } from '../../../app/domain/device/device_id.js'
|
||||
|
||||
const MONGO_URI = 'mongodb://adam:adam@localhost:27017/adam-acs-test?authSource=admin'
|
||||
let client: MongoClient
|
||||
let db: Db
|
||||
|
||||
describe('Device Model (integration)', () => {
|
||||
beforeAll(async () => {
|
||||
client = new MongoClient(MONGO_URI)
|
||||
await client.connect()
|
||||
db = client.db('adam-acs-test')
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await db.dropDatabase()
|
||||
await client.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await db.collection('devices').deleteMany({})
|
||||
})
|
||||
|
||||
it('creates and retrieves a device', async () => {
|
||||
const col = db.collection('devices')
|
||||
const device = {
|
||||
identifier: 'test-hash-001',
|
||||
deviceId: { manufacturer: 'Sagemcom', oui: 'F8B7FF', productClass: 'Livebox', serialNumber: 'SN001' },
|
||||
properties: [{ name: 'Device.DeviceInfo.Manufacturer', value: 'Sagemcom', type: 'xsd:string' }],
|
||||
parameters: [],
|
||||
gpnStatus: 'unknown',
|
||||
dataModel: 'tr-181',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
await col.insertOne(device)
|
||||
|
||||
const found = await col.findOne({ identifier: 'test-hash-001' })
|
||||
expect(found).not.toBeNull()
|
||||
expect(found!.deviceId.manufacturer).toBe('Sagemcom')
|
||||
expect(found!.properties).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('updates device properties', async () => {
|
||||
const col = db.collection('devices')
|
||||
await col.insertOne({
|
||||
identifier: 'dev-002',
|
||||
deviceId: { manufacturer: 'Huawei', oui: '202BC1', productClass: 'BM632w', serialNumber: '000000' },
|
||||
properties: [{ name: 'InternetGateway.DeviceInfo.SW', value: '1.0', type: 'xsd:string' }],
|
||||
parameters: [],
|
||||
gpnStatus: 'supported',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
||||
await col.updateOne(
|
||||
{ identifier: 'dev-002' },
|
||||
{ $set: { 'properties.0.value': '2.0', updatedAt: new Date() } }
|
||||
)
|
||||
|
||||
const found = await col.findOne({ identifier: 'dev-002' })
|
||||
expect(found!.properties[0].value).toBe('2.0')
|
||||
})
|
||||
|
||||
it('stores and retrieves references', async () => {
|
||||
const col = db.collection('devices')
|
||||
await col.insertOne({
|
||||
identifier: 'dev-003',
|
||||
deviceId: { manufacturer: 'Test', oui: 'AABB', productClass: 'X', serialNumber: 'SN' },
|
||||
properties: [],
|
||||
parameters: [],
|
||||
references: [
|
||||
{ provider: 'wassup', status: 'ok', resolvedAt: new Date(), resolvedBy: 'ip:10.0.0.1', properties: { sai: '123', spr: 'FR' } },
|
||||
],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
||||
const found = await col.findOne({ identifier: 'dev-003' })
|
||||
expect(found!.references).toHaveLength(1)
|
||||
expect(found!.references[0].provider).toBe('wassup')
|
||||
expect(found!.references[0].properties.sai).toBe('123')
|
||||
})
|
||||
|
||||
it('deletes a device', async () => {
|
||||
const col = db.collection('devices')
|
||||
await col.insertOne({ identifier: 'dev-del', deviceId: {}, properties: [], parameters: [] })
|
||||
const result = await col.deleteOne({ identifier: 'dev-del' })
|
||||
expect(result.deletedCount).toBe(1)
|
||||
})
|
||||
|
||||
it('paginates devices', async () => {
|
||||
const col = db.collection('devices')
|
||||
await col.deleteMany({}) // explicit clean
|
||||
const docs = Array.from({ length: 15 }, (_, i) => ({
|
||||
identifier: `dev-${i}`,
|
||||
deviceId: { manufacturer: 'M', oui: 'O', productClass: 'P', serialNumber: `S${i}` },
|
||||
properties: [],
|
||||
parameters: [],
|
||||
}))
|
||||
await col.insertMany(docs)
|
||||
|
||||
const total = await col.countDocuments({})
|
||||
expect(total).toBe(15)
|
||||
|
||||
const page1 = await col.find({}).skip(0).limit(10).toArray()
|
||||
expect(page1).toHaveLength(10)
|
||||
|
||||
const page2 = await col.find({}).skip(10).limit(10).toArray()
|
||||
expect(page2).toHaveLength(5)
|
||||
})
|
||||
|
||||
it('finds device by deviceId fields', async () => {
|
||||
const col = db.collection('devices')
|
||||
await col.insertOne({
|
||||
identifier: 'dev-find',
|
||||
deviceId: { manufacturer: 'Sagemcom', oui: 'F8B7FF', productClass: 'LB6', serialNumber: 'UNIQUE-SN' },
|
||||
properties: [],
|
||||
parameters: [],
|
||||
})
|
||||
|
||||
const found = await col.findOne({ 'deviceId.serialNumber': 'UNIQUE-SN' })
|
||||
expect(found).not.toBeNull()
|
||||
expect(found!.identifier).toBe('dev-find')
|
||||
})
|
||||
|
||||
it('updates references array', async () => {
|
||||
const col = db.collection('devices')
|
||||
await col.insertOne({ identifier: 'dev-ref', deviceId: {}, properties: [], parameters: [], references: [] })
|
||||
|
||||
const newRefs = [
|
||||
{ provider: 'wassup', status: 'ok', resolvedAt: new Date(), resolvedBy: 'ip:1.2.3.4', properties: { sai: '999' } },
|
||||
]
|
||||
await col.updateOne({ identifier: 'dev-ref' }, { $set: { references: newRefs } })
|
||||
|
||||
const found = await col.findOne({ identifier: 'dev-ref' })
|
||||
expect(found!.references[0].properties.sai).toBe('999')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Integration test setup — connects to local MongoDB and provides
|
||||
* a clean database for each test suite.
|
||||
*/
|
||||
import { MongoClient, Db } from 'mongodb'
|
||||
|
||||
const MONGO_URI = 'mongodb://adam:adam@localhost:27017/adam-acs-test?authSource=admin'
|
||||
const DB_NAME = 'adam-acs-test'
|
||||
|
||||
let client: MongoClient
|
||||
let db: Db
|
||||
|
||||
export async function setupMongo(): Promise<Db> {
|
||||
client = new MongoClient(MONGO_URI)
|
||||
await client.connect()
|
||||
db = client.db(DB_NAME)
|
||||
|
||||
// Monkey-patch getMongoDb to use our test DB
|
||||
const provider = await import('../../providers/mongodb_provider.js')
|
||||
;(provider as any).getMongoDb = () => db
|
||||
// Also patch the module-level variable via a workaround
|
||||
Object.defineProperty(await import('../../providers/mongodb_provider.js'), 'getMongoDb', {
|
||||
value: () => db,
|
||||
})
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
export async function teardownMongo(): Promise<void> {
|
||||
if (db) {
|
||||
await db.dropDatabase()
|
||||
}
|
||||
if (client) {
|
||||
await client.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanCollections(): Promise<void> {
|
||||
if (!db) return
|
||||
const collections = await db.listCollections().toArray()
|
||||
for (const col of collections) {
|
||||
await db.collection(col.name).deleteMany({})
|
||||
}
|
||||
}
|
||||
|
||||
export function getTestDb(): Db {
|
||||
return db
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { Command, CommandStatus, CommandType, Priority } from '../../../app/domain/commands/command.js'
|
||||
import { DeviceId } from '../../../app/domain/device/device_id.js'
|
||||
|
||||
function makeCommand(status = CommandStatus.PENDING) {
|
||||
return new Command('cmd-1', new DeviceId('Mfr', 'OUI', 'PC', 'SN'), CommandType.REFRESH, {}, Priority.NORMAL, new Date(), status)
|
||||
}
|
||||
|
||||
describe('Command', () => {
|
||||
it('creates with PENDING status by default', () => {
|
||||
const cmd = makeCommand()
|
||||
expect(cmd.status).toBe(CommandStatus.PENDING)
|
||||
expect(cmd.canExecute()).toBe(true)
|
||||
expect(cmd.isCompleted()).toBe(false)
|
||||
})
|
||||
|
||||
it('markAsSent transitions PENDING → SENT', () => {
|
||||
const cmd = makeCommand()
|
||||
cmd.markAsSent()
|
||||
expect(cmd.status).toBe(CommandStatus.SENT)
|
||||
expect(cmd.executedAt).not.toBeNull()
|
||||
})
|
||||
|
||||
it('markAsSent throws if not PENDING', () => {
|
||||
const cmd = makeCommand(CommandStatus.SENT)
|
||||
expect(() => cmd.markAsSent()).toThrow('Cannot mark command as sent')
|
||||
})
|
||||
|
||||
it('markAsWaiting transitions SENT → WAITING', () => {
|
||||
const cmd = makeCommand()
|
||||
cmd.markAsSent()
|
||||
cmd.markAsWaiting()
|
||||
expect(cmd.status).toBe(CommandStatus.WAITING)
|
||||
})
|
||||
|
||||
it('markAsWaiting throws if not SENT', () => {
|
||||
const cmd = makeCommand()
|
||||
expect(() => cmd.markAsWaiting()).toThrow('Cannot mark command as waiting')
|
||||
})
|
||||
|
||||
it('complete transitions SENT → COMPLETED', () => {
|
||||
const cmd = makeCommand()
|
||||
cmd.markAsSent()
|
||||
cmd.complete({ success: true })
|
||||
expect(cmd.status).toBe(CommandStatus.COMPLETED)
|
||||
expect(cmd.result).toEqual({ success: true })
|
||||
expect(cmd.isCompleted()).toBe(true)
|
||||
})
|
||||
|
||||
it('complete throws if PENDING', () => {
|
||||
const cmd = makeCommand()
|
||||
expect(() => cmd.complete({})).toThrow('Cannot complete command')
|
||||
})
|
||||
|
||||
it('fail transitions to FAILED', () => {
|
||||
const cmd = makeCommand()
|
||||
cmd.markAsSent()
|
||||
cmd.fail('timeout')
|
||||
expect(cmd.status).toBe(CommandStatus.FAILED)
|
||||
expect(cmd.error).toBe('timeout')
|
||||
expect(cmd.isCompleted()).toBe(true)
|
||||
})
|
||||
|
||||
it('fail throws if already COMPLETED', () => {
|
||||
const cmd = makeCommand()
|
||||
cmd.markAsSent()
|
||||
cmd.complete({})
|
||||
expect(() => cmd.fail('oops')).toThrow('Cannot fail a successful command')
|
||||
})
|
||||
|
||||
it('cancel transitions PENDING → CANCELLED', () => {
|
||||
const cmd = makeCommand()
|
||||
cmd.cancel()
|
||||
expect(cmd.status).toBe(CommandStatus.CANCELLED)
|
||||
expect(cmd.isCompleted()).toBe(true)
|
||||
})
|
||||
|
||||
it('cancel throws if COMPLETED', () => {
|
||||
const cmd = makeCommand()
|
||||
cmd.markAsSent()
|
||||
cmd.complete({})
|
||||
expect(() => cmd.cancel()).toThrow('Cannot cancel command')
|
||||
})
|
||||
|
||||
it('hasExpired returns true for old commands', () => {
|
||||
const old = new Date(Date.now() - 60 * 60 * 1000) // 1h ago
|
||||
const cmd = new Command('x', new DeviceId('M', 'O', 'P', 'S'), CommandType.REFRESH, {}, Priority.NORMAL, old)
|
||||
expect(cmd.hasExpired(30)).toBe(true)
|
||||
})
|
||||
|
||||
it('hasExpired returns false for fresh commands', () => {
|
||||
const cmd = makeCommand()
|
||||
expect(cmd.hasExpired(30)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { DeviceId } from '../../../app/domain/device/device_id.js'
|
||||
|
||||
describe('DeviceId', () => {
|
||||
it('creates a valid DeviceId', () => {
|
||||
const id = new DeviceId('Sagemcom', 'F8B7FF', 'Livebox', 'SN001')
|
||||
expect(id.manufacturer).toBe('Sagemcom')
|
||||
expect(id.oui).toBe('F8B7FF')
|
||||
expect(id.productClass).toBe('Livebox')
|
||||
expect(id.serialNumber).toBe('SN001')
|
||||
})
|
||||
|
||||
it('throws if manufacturer is empty', () => {
|
||||
expect(() => new DeviceId('', 'OUI', 'PC', 'SN')).toThrow('Manufacturer is required')
|
||||
})
|
||||
|
||||
it('throws if manufacturer is whitespace', () => {
|
||||
expect(() => new DeviceId(' ', 'OUI', 'PC', 'SN')).toThrow('Manufacturer is required')
|
||||
})
|
||||
|
||||
it('throws if serialNumber is empty', () => {
|
||||
expect(() => new DeviceId('Mfr', 'OUI', 'PC', '')).toThrow('Serial number is required')
|
||||
})
|
||||
|
||||
it('toIdentifier returns formatted string', () => {
|
||||
const id = new DeviceId('Sagemcom', 'F8B7FF', 'Livebox 4', 'SN001')
|
||||
expect(id.toIdentifier()).toBe('Sagemcom-F8B7FF-Livebox 4-SN001')
|
||||
})
|
||||
|
||||
it('toString delegates to toIdentifier', () => {
|
||||
const id = new DeviceId('Mfr', 'OUI', 'PC', 'SN')
|
||||
expect(id.toString()).toBe(id.toIdentifier())
|
||||
})
|
||||
|
||||
it('equals returns true for same values', () => {
|
||||
const a = new DeviceId('Mfr', 'OUI', 'PC', 'SN')
|
||||
const b = new DeviceId('Mfr', 'OUI', 'PC', 'SN')
|
||||
expect(a.equals(b)).toBe(true)
|
||||
})
|
||||
|
||||
it('equals returns false for different values', () => {
|
||||
const a = new DeviceId('Mfr', 'OUI', 'PC', 'SN1')
|
||||
const b = new DeviceId('Mfr', 'OUI', 'PC', 'SN2')
|
||||
expect(a.equals(b)).toBe(false)
|
||||
})
|
||||
|
||||
it('fromInform creates DeviceId from inform data', () => {
|
||||
const id = DeviceId.fromInform({ Manufacturer: 'Huawei', OUI: '202BC1', ProductClass: 'BM632w', SerialNumber: '000000' })
|
||||
expect(id.manufacturer).toBe('Huawei')
|
||||
expect(id.oui).toBe('202BC1')
|
||||
})
|
||||
|
||||
it('fromInform handles missing fields gracefully', () => {
|
||||
// fromInform with empty strings will fail validation since manufacturer is required
|
||||
expect(() => DeviceId.fromInform({})).toThrow('Manufacturer is required')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { DeviceParameter, DeviceProperty } from '../../../app/domain/device/device_parameter.js'
|
||||
|
||||
describe('DeviceParameter', () => {
|
||||
it('creates with name and value', () => {
|
||||
const p = new DeviceParameter('Device.Info.Name', 'hello')
|
||||
expect(p.name).toBe('Device.Info.Name')
|
||||
expect(p.value).toBe('hello')
|
||||
})
|
||||
|
||||
it('updateValue changes value and updatedAt', () => {
|
||||
const p = new DeviceParameter('x', 'old')
|
||||
const before = p.updatedAt
|
||||
p.updateValue('new')
|
||||
expect(p.value).toBe('new')
|
||||
expect(p.updatedAt.getTime()).toBeGreaterThanOrEqual(before.getTime())
|
||||
})
|
||||
|
||||
it('updateAttributes sets notification and accessList', () => {
|
||||
const p = new DeviceParameter('x', 'v')
|
||||
p.updateAttributes(2, ['admin'])
|
||||
expect(p.notification).toBe(2)
|
||||
expect(p.accessList).toEqual(['admin'])
|
||||
})
|
||||
|
||||
it('updateAttributes only sets provided fields', () => {
|
||||
const p = new DeviceParameter('x', 'v')
|
||||
p.updateAttributes(1)
|
||||
expect(p.notification).toBe(1)
|
||||
expect(p.accessList).toBeUndefined()
|
||||
})
|
||||
|
||||
it('toSnapshot returns serializable object', () => {
|
||||
const p = new DeviceParameter('Device.Info', 'val')
|
||||
const snap = p.toSnapshot()
|
||||
expect(snap.name).toBe('Device.Info')
|
||||
expect(snap.value).toBe('val')
|
||||
expect(snap.createdAt).toBeInstanceOf(Date)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DeviceProperty', () => {
|
||||
it('creates and reads value', () => {
|
||||
const p = new DeviceProperty('Device.SW', '1.0')
|
||||
expect(p.value).toBe('1.0')
|
||||
})
|
||||
|
||||
it('updateValue changes value', () => {
|
||||
const p = new DeviceProperty('x', 'old')
|
||||
p.updateValue('new')
|
||||
expect(p.value).toBe('new')
|
||||
})
|
||||
|
||||
it('updateAttributes sets fields', () => {
|
||||
const p = new DeviceProperty('x', 'v')
|
||||
p.updateAttributes(0, [])
|
||||
expect(p.notification).toBe(0)
|
||||
expect(p.accessList).toEqual([])
|
||||
})
|
||||
|
||||
it('getConnectionRequestURL returns URL for matching name', () => {
|
||||
const p = new DeviceProperty('Device.ManagementServer.ConnectionRequestURL', 'http://example.com:7547/')
|
||||
expect(p.getConnectionRequestURL()).not.toBeNull()
|
||||
expect(p.getConnectionRequestURL()!.hostname).toBe('example.com')
|
||||
})
|
||||
|
||||
it('getConnectionRequestURL returns null for non-matching name', () => {
|
||||
const p = new DeviceProperty('Device.Info.Name', 'http://example.com')
|
||||
expect(p.getConnectionRequestURL()).toBeNull()
|
||||
})
|
||||
|
||||
it('getConnectionRequestURL returns null for invalid URL', () => {
|
||||
const p = new DeviceProperty('Device.ManagementServer.ConnectionRequestURL', 'not-a-url')
|
||||
expect(p.getConnectionRequestURL()).toBeNull()
|
||||
})
|
||||
|
||||
it('getConnectionRequestURL returns null for empty value', () => {
|
||||
const p = new DeviceProperty('Device.ManagementServer.ConnectionRequestURL', '')
|
||||
expect(p.getConnectionRequestURL()).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { ModeType, isModeType, parseMode } from '../../../app/domain/shared/mode.js'
|
||||
|
||||
describe('ModeType', () => {
|
||||
it('isModeType returns true for NI', () => {
|
||||
expect(isModeType('NI')).toBe(true)
|
||||
})
|
||||
|
||||
it('isModeType returns true for I', () => {
|
||||
expect(isModeType('I')).toBe(true)
|
||||
})
|
||||
|
||||
it('isModeType returns false for invalid', () => {
|
||||
expect(isModeType('X')).toBe(false)
|
||||
expect(isModeType('')).toBe(false)
|
||||
expect(isModeType(null)).toBe(false)
|
||||
expect(isModeType(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
it('parseMode returns ModeType.NI for "ni"', () => {
|
||||
expect(parseMode('ni')).toBe(ModeType.NI)
|
||||
expect(parseMode('NI')).toBe(ModeType.NI)
|
||||
})
|
||||
|
||||
it('parseMode returns ModeType.I for "i"', () => {
|
||||
expect(parseMode('i')).toBe(ModeType.I)
|
||||
expect(parseMode('I')).toBe(ModeType.I)
|
||||
})
|
||||
|
||||
it('parseMode returns undefined for invalid', () => {
|
||||
expect(parseMode('X')).toBeUndefined()
|
||||
expect(parseMode('')).toBeUndefined()
|
||||
expect(parseMode(undefined)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { CWMPMessageBuilder } from '../../../app/protocol/common/cwmp_message_builder.js'
|
||||
|
||||
describe('CWMPMessageBuilder', () => {
|
||||
it('informResponse builds valid response', () => {
|
||||
const xml = CWMPMessageBuilder.informResponse('id-1')
|
||||
expect(xml).toContain('InformResponse')
|
||||
expect(xml).toContain('MaxEnvelopes')
|
||||
expect(xml).toContain('id-1')
|
||||
})
|
||||
|
||||
it('getParameterNames builds request', () => {
|
||||
const xml = CWMPMessageBuilder.getParameterNames('id-2', 'Device.', true)
|
||||
expect(xml).toContain('GetParameterNames')
|
||||
expect(xml).toContain('Device.')
|
||||
expect(xml).toContain('<NextLevel>1</NextLevel>')
|
||||
})
|
||||
|
||||
it('getParameterNames with nextLevel=false', () => {
|
||||
const xml = CWMPMessageBuilder.getParameterNames('id-3', 'Device.', false)
|
||||
expect(xml).toContain('<NextLevel>0</NextLevel>')
|
||||
})
|
||||
|
||||
it('getParameterValues builds request with names', () => {
|
||||
const xml = CWMPMessageBuilder.getParameterValues('id-4', ['Device.Info.SW', 'Device.Info.HW'])
|
||||
expect(xml).toContain('GetParameterValues')
|
||||
expect(xml).toContain('Device.Info.SW')
|
||||
expect(xml).toContain('Device.Info.HW')
|
||||
expect(xml).toContain('arrayType="xsd:string[2]"')
|
||||
})
|
||||
|
||||
it('setParameterValues builds request with params', () => {
|
||||
const xml = CWMPMessageBuilder.setParameterValues('id-5', [
|
||||
{ name: 'Device.X', value: '42', type: 'xsd:int' },
|
||||
])
|
||||
expect(xml).toContain('SetParameterValues')
|
||||
expect(xml).toContain('Device.X')
|
||||
expect(xml).toContain('42')
|
||||
expect(xml).toContain('xsd:int')
|
||||
expect(xml).toContain('ParameterKey')
|
||||
})
|
||||
|
||||
it('download builds request with all params', () => {
|
||||
const xml = CWMPMessageBuilder.download('id-6', {
|
||||
url: 'http://example.com/fw.bin',
|
||||
commandKey: 'fw-upgrade',
|
||||
fileType: '1 Firmware Upgrade Image',
|
||||
fileSize: 1024,
|
||||
})
|
||||
expect(xml).toContain('Download')
|
||||
expect(xml).toContain('http://example.com/fw.bin')
|
||||
expect(xml).toContain('fw-upgrade')
|
||||
expect(xml).toContain('1024')
|
||||
})
|
||||
|
||||
it('upload builds request', () => {
|
||||
const xml = CWMPMessageBuilder.upload('id-7', { url: 'http://upload.com/cfg', commandKey: 'cfg-bk' })
|
||||
expect(xml).toContain('Upload')
|
||||
expect(xml).toContain('http://upload.com/cfg')
|
||||
expect(xml).toContain('cfg-bk')
|
||||
})
|
||||
|
||||
it('reboot builds request', () => {
|
||||
const xml = CWMPMessageBuilder.reboot('id-8', 'reboot-key')
|
||||
expect(xml).toContain('Reboot')
|
||||
expect(xml).toContain('reboot-key')
|
||||
})
|
||||
|
||||
it('factoryReset builds request', () => {
|
||||
const xml = CWMPMessageBuilder.factoryReset('id-9')
|
||||
expect(xml).toContain('FactoryReset')
|
||||
})
|
||||
|
||||
it('scheduleInform builds request', () => {
|
||||
const xml = CWMPMessageBuilder.scheduleInform('id-10', 300, 'sched-key')
|
||||
expect(xml).toContain('ScheduleInform')
|
||||
expect(xml).toContain('300')
|
||||
expect(xml).toContain('sched-key')
|
||||
})
|
||||
|
||||
it('getParameterAttributes builds request', () => {
|
||||
const xml = CWMPMessageBuilder.getParameterAttributes('id-11', ['Device.Info.'])
|
||||
expect(xml).toContain('GetParameterAttributes')
|
||||
expect(xml).toContain('Device.Info.')
|
||||
})
|
||||
|
||||
it('setParameterAttributes builds request', () => {
|
||||
const xml = CWMPMessageBuilder.setParameterAttributes('id-12', [
|
||||
{ name: 'Device.X', notificationChange: true, notification: 2, accessListChange: false, accessList: [] },
|
||||
])
|
||||
expect(xml).toContain('SetParameterAttributes')
|
||||
expect(xml).toContain('Device.X')
|
||||
expect(xml).toContain('<NotificationChange>1</NotificationChange>')
|
||||
expect(xml).toContain('<Notification>2</Notification>')
|
||||
})
|
||||
|
||||
it('emptyResponse returns empty string', () => {
|
||||
expect(CWMPMessageBuilder.emptyResponse()).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { parseSoapXml, extractCwmpMessage, buildSoapEnvelope } from '../../../app/protocol/soap.js'
|
||||
|
||||
describe('parseSoapXml', () => {
|
||||
it('parses a basic Inform SOAP envelope', () => {
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cwmp="urn:dslforum-org:cwmp-1-0">
|
||||
<soap-env:Header>
|
||||
<cwmp:ID soap-env:mustUnderstand="1">0001</cwmp:ID>
|
||||
</soap-env:Header>
|
||||
<soap-env:Body>
|
||||
<cwmp:Inform>
|
||||
<DeviceId><Manufacturer>Test</Manufacturer></DeviceId>
|
||||
</cwmp:Inform>
|
||||
</soap-env:Body>
|
||||
</soap-env:Envelope>`
|
||||
const result = parseSoapXml(xml)
|
||||
expect(result.header.id).toBe('0001')
|
||||
expect(result.body).toBeDefined()
|
||||
expect(result.cwmpVersion).toBe('1.0')
|
||||
})
|
||||
|
||||
it('parses soapenv prefix variant', () => {
|
||||
const xml = `<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cwmp="urn:dslforum-org:cwmp-1-0">
|
||||
<soapenv:Header><cwmp:ID soapenv:mustUnderstand="1">42</cwmp:ID></soapenv:Header>
|
||||
<soapenv:Body><cwmp:InformResponse><MaxEnvelopes>1</MaxEnvelopes></cwmp:InformResponse></soapenv:Body>
|
||||
</soapenv:Envelope>`
|
||||
const result = parseSoapXml(xml)
|
||||
expect(String(result.header.id)).toBe('42')
|
||||
})
|
||||
|
||||
it('handles missing header gracefully', () => {
|
||||
const xml = `<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soap-env:Body><cwmp:GetParameterValuesResponse xmlns:cwmp="urn:dslforum-org:cwmp-1-0"></cwmp:GetParameterValuesResponse></soap-env:Body>
|
||||
</soap-env:Envelope>`
|
||||
const result = parseSoapXml(xml)
|
||||
expect(result.header.id).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractCwmpMessage', () => {
|
||||
it('extracts Inform message', () => {
|
||||
const body = { 'cwmp:Inform': { DeviceId: { Manufacturer: 'Test' } } }
|
||||
const msg = extractCwmpMessage(body)
|
||||
expect(msg).not.toBeNull()
|
||||
expect(msg!.type).toBe('Inform')
|
||||
expect(msg!.data.DeviceId.Manufacturer).toBe('Test')
|
||||
})
|
||||
|
||||
it('extracts GetParameterValuesResponse', () => {
|
||||
const body = { 'cwmp:GetParameterValuesResponse': { ParameterList: [] } }
|
||||
const msg = extractCwmpMessage(body)
|
||||
expect(msg!.type).toBe('GetParameterValuesResponse')
|
||||
})
|
||||
|
||||
it('returns null for empty body', () => {
|
||||
expect(extractCwmpMessage({})).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for non-cwmp keys', () => {
|
||||
expect(extractCwmpMessage({ 'other:Thing': {} })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildSoapEnvelope', () => {
|
||||
it('builds valid XML envelope', () => {
|
||||
const xml = buildSoapEnvelope('id-123', '<cwmp:Test/>')
|
||||
expect(xml).toContain('soap-env:Envelope')
|
||||
expect(xml).toContain('id-123')
|
||||
expect(xml).toContain('<cwmp:Test/>')
|
||||
})
|
||||
|
||||
it('builds envelope without id', () => {
|
||||
const xml = buildSoapEnvelope(undefined, '<cwmp:Reboot/>')
|
||||
expect(xml).toContain('<cwmp:Reboot/>')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { loadReferencesConfig, reloadReferencesConfig } from '../../../../app/services/references/config_loader.js'
|
||||
|
||||
describe('config_loader', () => {
|
||||
it('loadReferencesConfig loads config from file', () => {
|
||||
const config = loadReferencesConfig()
|
||||
expect(config).toBeDefined()
|
||||
expect(config.providers).toBeInstanceOf(Array)
|
||||
expect(config.providers.length).toBeGreaterThan(0)
|
||||
expect(config.providers[0].name).toBe('wassup')
|
||||
})
|
||||
|
||||
it('loadReferencesConfig returns cached result on second call', () => {
|
||||
const a = loadReferencesConfig()
|
||||
const b = loadReferencesConfig()
|
||||
expect(a).toBe(b) // same reference (cached)
|
||||
})
|
||||
|
||||
it('reloadReferencesConfig clears cache and reloads', () => {
|
||||
const a = loadReferencesConfig()
|
||||
const b = reloadReferencesConfig()
|
||||
expect(b.providers).toBeInstanceOf(Array)
|
||||
// After reload, a new object is returned
|
||||
expect(b).not.toBe(a)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user