mqtt-devices-parser 1.0.27 → 1.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test.yml +34 -0
- package/Changelog.md +12 -0
- package/jest.config.js +22 -0
- package/models/logs_mqtt.models.js +29 -0
- package/models/logs_mqtt_msgs.models.js +29 -0
- package/models/sensors.models.js +1 -1
- package/models/variants.models.js +0 -1
- package/package.json +6 -2
- package/src/db/device.js +50 -0
- package/src/db/firmware.js +6 -6
- package/src/device/Readme.md +0 -4
- package/src/device/Test.md +95 -0
- package/src/device/device.js +73 -16
- package/src/device/device.test.js +456 -0
- package/src/kafka/consumer.js +1 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up Node.js
|
|
17
|
+
uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: '20'
|
|
20
|
+
cache: 'npm'
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: npm ci
|
|
24
|
+
|
|
25
|
+
- name: Run tests with coverage
|
|
26
|
+
run: npm test
|
|
27
|
+
|
|
28
|
+
- name: Upload coverage report
|
|
29
|
+
uses: actions/upload-artifact@v4
|
|
30
|
+
if: always()
|
|
31
|
+
with:
|
|
32
|
+
name: coverage-report
|
|
33
|
+
path: coverage/
|
|
34
|
+
retention-days: 7
|
package/Changelog.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.29
|
|
4
|
+
fix(models/sensors): allow null model_id
|
|
5
|
+
fix: synching mqtt topics
|
|
6
|
+
feat: stores mqtt messages
|
|
7
|
+
|
|
8
|
+
## 1.0.28
|
|
9
|
+
test: Add comprehensive Jest test suite for device.js module (#2)
|
|
10
|
+
feat: scope FOTA checks to device variant_id (#11)
|
|
11
|
+
doc(device): recover readme add test.md file
|
|
12
|
+
fix(models/variants): remove unique true from column name
|
|
13
|
+
ci: fix vulnerabilities
|
|
14
|
+
|
|
3
15
|
## 1.0.27
|
|
4
16
|
feat(db): adds variants table
|
|
5
17
|
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
testEnvironment: 'node',
|
|
3
|
+
collectCoverage: true,
|
|
4
|
+
collectCoverageFrom: [
|
|
5
|
+
'src/**/*.js',
|
|
6
|
+
'!src/**/*.test.js',
|
|
7
|
+
'!src/unitTest/**'
|
|
8
|
+
],
|
|
9
|
+
coverageDirectory: 'coverage',
|
|
10
|
+
coverageReporters: ['text', 'lcov', 'html'],
|
|
11
|
+
testMatch: [
|
|
12
|
+
'**/__tests__/**/*.js',
|
|
13
|
+
'**/?(*.)+(spec|test).js'
|
|
14
|
+
],
|
|
15
|
+
testPathIgnorePatterns: [
|
|
16
|
+
'/node_modules/',
|
|
17
|
+
'/src/unitTest/'
|
|
18
|
+
],
|
|
19
|
+
clearMocks: true,
|
|
20
|
+
resetMocks: true,
|
|
21
|
+
restoreMocks: true
|
|
22
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
module.exports = (sequelize,DataTypes)=>{
|
|
3
|
+
return sequelize.define("logs_mqtt", {
|
|
4
|
+
mqtt_id: {
|
|
5
|
+
type: DataTypes.INTEGER,
|
|
6
|
+
allowNull: false,
|
|
7
|
+
},
|
|
8
|
+
device_id: {
|
|
9
|
+
type: DataTypes.INTEGER,
|
|
10
|
+
allowNull: false,
|
|
11
|
+
},
|
|
12
|
+
source: {
|
|
13
|
+
type: DataTypes.STRING,
|
|
14
|
+
allowNull: false
|
|
15
|
+
},
|
|
16
|
+
action: {
|
|
17
|
+
type: DataTypes.STRING,
|
|
18
|
+
allowNull: false
|
|
19
|
+
},
|
|
20
|
+
payload: {
|
|
21
|
+
type: DataTypes.STRING,
|
|
22
|
+
allowNull: true
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
tableName: 'logs_mqtt',
|
|
27
|
+
freezeTableName: true
|
|
28
|
+
})
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
module.exports = (sequelize,DataTypes)=>{
|
|
3
|
+
return sequelize.define("logs_mqtt_msgs", {
|
|
4
|
+
device_id: {
|
|
5
|
+
type: DataTypes.INTEGER,
|
|
6
|
+
allowNull: false,
|
|
7
|
+
},
|
|
8
|
+
topic: {
|
|
9
|
+
type: DataTypes.STRING,
|
|
10
|
+
allowNull: false
|
|
11
|
+
},
|
|
12
|
+
payload: {
|
|
13
|
+
type: DataTypes.STRING,
|
|
14
|
+
allowNull: true
|
|
15
|
+
},
|
|
16
|
+
qos: {
|
|
17
|
+
type: DataTypes.INTEGER,
|
|
18
|
+
allowNull: false
|
|
19
|
+
},
|
|
20
|
+
retain: {
|
|
21
|
+
type: DataTypes.INTEGER,
|
|
22
|
+
allowNull: false
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
tableName: 'logs_mqtt_msgs',
|
|
27
|
+
freezeTableName: true
|
|
28
|
+
})
|
|
29
|
+
}
|
package/models/sensors.models.js
CHANGED
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mqtt-devices-parser",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.29",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "
|
|
7
|
+
"test": "jest",
|
|
8
|
+
"test:watch": "jest --watch"
|
|
8
9
|
},
|
|
9
10
|
"keywords": [],
|
|
10
11
|
"author": "",
|
|
@@ -30,5 +31,8 @@
|
|
|
30
31
|
"sequelize": {
|
|
31
32
|
"uuid": "^11.1.1"
|
|
32
33
|
}
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"jest": "^30.0.5"
|
|
33
37
|
}
|
|
34
38
|
}
|
package/src/db/device.js
CHANGED
|
@@ -216,6 +216,56 @@ var self = module.exports = {
|
|
|
216
216
|
});
|
|
217
217
|
},
|
|
218
218
|
|
|
219
|
+
addMqttLog : async(deviceId,dbTopicId,from,action,payload)=>{
|
|
220
|
+
return new Promise((resolve,reject) => {
|
|
221
|
+
|
|
222
|
+
const timestamp = moment().utc().format('YYYY-MM-DD HH:mm:ss')
|
|
223
|
+
let obj = {
|
|
224
|
+
createdAt : timestamp,
|
|
225
|
+
updatedAt : timestamp
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
obj['device_id'] = deviceId;
|
|
229
|
+
obj['mqtt_id'] = dbTopicId;
|
|
230
|
+
obj['source'] = from;
|
|
231
|
+
obj['action'] = action;
|
|
232
|
+
obj['payload'] = payload;
|
|
233
|
+
|
|
234
|
+
$.db.insert("logs_mqtt",obj)
|
|
235
|
+
.then (rows => {
|
|
236
|
+
return resolve(rows);
|
|
237
|
+
})
|
|
238
|
+
.catch(error => {
|
|
239
|
+
return reject(error);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
addMqttMsgLog : async(deviceId,topic,payload,retain,qos)=>{
|
|
245
|
+
return new Promise((resolve,reject) => {
|
|
246
|
+
|
|
247
|
+
const timestamp = moment().utc().format('YYYY-MM-DD HH:mm:ss')
|
|
248
|
+
let obj = {
|
|
249
|
+
createdAt : timestamp,
|
|
250
|
+
updatedAt : timestamp
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
obj['device_id'] = deviceId;
|
|
254
|
+
obj['topic'] = topic;
|
|
255
|
+
obj['payload'] = payload;
|
|
256
|
+
obj['retain'] = retain;
|
|
257
|
+
obj['qos'] = qos;
|
|
258
|
+
|
|
259
|
+
$.db.insert("logs_mqtt_msgs",obj)
|
|
260
|
+
.then (rows => {
|
|
261
|
+
return resolve(rows);
|
|
262
|
+
})
|
|
263
|
+
.catch(error => {
|
|
264
|
+
return reject(error);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
|
|
219
269
|
addLogIfChanged : async(id,column,value)=>{
|
|
220
270
|
return new Promise(async (resolve,reject) => {
|
|
221
271
|
|
package/src/db/firmware.js
CHANGED
|
@@ -28,18 +28,18 @@ var self = module.exports = {
|
|
|
28
28
|
});
|
|
29
29
|
},
|
|
30
30
|
|
|
31
|
-
getLatestVersion : async (modelId,release)=>{
|
|
31
|
+
getLatestVersion : async (modelId,release,variantId)=>{
|
|
32
32
|
|
|
33
33
|
return new Promise((resolve,reject) => {
|
|
34
34
|
|
|
35
35
|
let query = "";
|
|
36
36
|
let table = [];
|
|
37
37
|
|
|
38
|
-
query = `SELECT version,filename,token,id FROM firmwares where model_id = ? and build_release = ? ORDER BY CAST(SUBSTRING_INDEX(version, '.', 1) AS UNSIGNED) DESC,
|
|
38
|
+
query = `SELECT version,filename,token,id FROM firmwares where model_id = ? and build_release = ? and variant_id = ? ORDER BY CAST(SUBSTRING_INDEX(version, '.', 1) AS UNSIGNED) DESC,
|
|
39
39
|
CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(version, '.', 2), '.', -1) AS UNSIGNED) DESC,
|
|
40
40
|
CAST(SUBSTRING_INDEX(version, '.', -1) AS UNSIGNED) DESC
|
|
41
41
|
LIMIT 1`;
|
|
42
|
-
table = [modelId,release];
|
|
42
|
+
table = [modelId,release,variantId];
|
|
43
43
|
|
|
44
44
|
query = mysql.format(query,table);
|
|
45
45
|
|
|
@@ -57,18 +57,18 @@ var self = module.exports = {
|
|
|
57
57
|
});
|
|
58
58
|
},
|
|
59
59
|
|
|
60
|
-
getLatestAppVersion : async (modelId,release)=>{
|
|
60
|
+
getLatestAppVersion : async (modelId,release,variantId)=>{
|
|
61
61
|
|
|
62
62
|
return new Promise((resolve,reject) => {
|
|
63
63
|
|
|
64
64
|
let query = "";
|
|
65
65
|
let table = [];
|
|
66
66
|
|
|
67
|
-
query = `SELECT app_version,filename,token,id FROM firmwares where model_id = ? and build_release = ? ORDER BY CAST(SUBSTRING_INDEX(app_version, '.', 1) AS UNSIGNED) DESC,
|
|
67
|
+
query = `SELECT app_version,filename,token,id FROM firmwares where model_id = ? and build_release = ? and variant_id = ? ORDER BY CAST(SUBSTRING_INDEX(app_version, '.', 1) AS UNSIGNED) DESC,
|
|
68
68
|
CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(app_version, '.', 2), '.', -1) AS UNSIGNED) DESC,
|
|
69
69
|
CAST(SUBSTRING_INDEX(app_version, '.', -1) AS UNSIGNED) DESC
|
|
70
70
|
LIMIT 1`;
|
|
71
|
-
table = [modelId,release];
|
|
71
|
+
table = [modelId,release,variantId];
|
|
72
72
|
|
|
73
73
|
query = mysql.format(query,table);
|
|
74
74
|
|
package/src/device/Readme.md
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Device Module Jest Test Plan
|
|
2
|
+
|
|
3
|
+
This test suite provides comprehensive coverage for the `src/device/device.js` module using Jest.
|
|
4
|
+
|
|
5
|
+
## Test Coverage
|
|
6
|
+
|
|
7
|
+
The test suite covers all major functions in the device module:
|
|
8
|
+
|
|
9
|
+
### 1. `init()` Function
|
|
10
|
+
- ✅ Database connection and project initialization
|
|
11
|
+
- ✅ Project module loading and initialization
|
|
12
|
+
- ✅ Database project insertion for new projects
|
|
13
|
+
|
|
14
|
+
### 2. `parseMessage()` Function
|
|
15
|
+
- ✅ Status message parsing (online/offline)
|
|
16
|
+
- ✅ Model message parsing with validation
|
|
17
|
+
- ✅ Version message parsing (when device has existing version)
|
|
18
|
+
- ✅ App version message parsing (when device has existing app version)
|
|
19
|
+
- ✅ Invalid topic format handling
|
|
20
|
+
- ✅ Unknown project handling
|
|
21
|
+
- ✅ Settings/set message handling (local settings updates)
|
|
22
|
+
- ✅ Firmware (fw) message handling with JSON payload
|
|
23
|
+
|
|
24
|
+
### 3. Settings Management Functions
|
|
25
|
+
- ✅ `updateLocalSettings()` - nested settings updates
|
|
26
|
+
- ✅ `updateLocalSettings()` - invalid JSON payload handling
|
|
27
|
+
- ✅ `updateLocalSettings()` - empty topic handling
|
|
28
|
+
- ✅ `updateRemoteSettings()` - remote settings updates
|
|
29
|
+
- ✅ `updateRemoteSettings()` - non-JSON payload handling
|
|
30
|
+
|
|
31
|
+
### 4. Log Management Functions
|
|
32
|
+
- ✅ `deleteLogs()` - old log entries cleanup from log tables
|
|
33
|
+
|
|
34
|
+
### 5. FOTA (Firmware Over The Air) Functions
|
|
35
|
+
- ✅ `checkFota()` - firmware update checking
|
|
36
|
+
- ✅ `checkFota()` - device release acceptance filtering
|
|
37
|
+
- ✅ `triggerFota()` - firmware update triggering for eligible devices
|
|
38
|
+
- ✅ `handleFotaSuccess()` - successful FOTA completion handling
|
|
39
|
+
- ✅ `handleFotaError()` - FOTA error handling
|
|
40
|
+
|
|
41
|
+
## Test Features
|
|
42
|
+
|
|
43
|
+
### Mocking Strategy
|
|
44
|
+
The test suite extensively mocks all database dependencies:
|
|
45
|
+
- `$.db` - Database connection and table operations
|
|
46
|
+
- `$.db_device` - Device database operations
|
|
47
|
+
- `$.db_project` - Project database operations
|
|
48
|
+
- `$.db_model` - Model database operations
|
|
49
|
+
- `$.db_sensor` - Sensor database operations
|
|
50
|
+
- `$.db_data` - Data storage operations
|
|
51
|
+
- `$.db_firmware` - Firmware database operations
|
|
52
|
+
- `$.db_fota` - FOTA database operations
|
|
53
|
+
|
|
54
|
+
### Coverage Statistics
|
|
55
|
+
- **Statements**: 73.3%
|
|
56
|
+
- **Branches**: 55.24%
|
|
57
|
+
- **Functions**: 78.94%
|
|
58
|
+
- **Lines**: 74.13%
|
|
59
|
+
|
|
60
|
+
### Test Configuration
|
|
61
|
+
- Uses Jest test framework
|
|
62
|
+
- Node.js test environment
|
|
63
|
+
- Coverage reports in text, lcov, and HTML formats
|
|
64
|
+
- Excludes existing integration tests in `src/unitTest/`
|
|
65
|
+
|
|
66
|
+
## Running Tests
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Run all tests
|
|
70
|
+
npm test
|
|
71
|
+
|
|
72
|
+
# Run tests with watch mode
|
|
73
|
+
npm run test:watch
|
|
74
|
+
|
|
75
|
+
# Run tests with coverage
|
|
76
|
+
npm test -- --coverage
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Architecture Notes
|
|
80
|
+
|
|
81
|
+
The tests follow Jest best practices:
|
|
82
|
+
- Comprehensive mocking of external dependencies
|
|
83
|
+
- Clear test descriptions and organization
|
|
84
|
+
- Edge case coverage
|
|
85
|
+
- Error handling validation
|
|
86
|
+
- Async/await pattern usage
|
|
87
|
+
|
|
88
|
+
## Known Limitations
|
|
89
|
+
|
|
90
|
+
Some complex scenarios are not tested due to the existing code architecture:
|
|
91
|
+
- Database connection failure handling (complex Promise wrapping)
|
|
92
|
+
- Device creation race conditions (async .then() chains)
|
|
93
|
+
- Project module require() failures (virtual module mocking limitations)
|
|
94
|
+
|
|
95
|
+
These limitations represent opportunities for code improvement in the actual device.js module.
|
package/src/device/device.js
CHANGED
|
@@ -33,7 +33,7 @@ var self = module.exports = {
|
|
|
33
33
|
});
|
|
34
34
|
},
|
|
35
35
|
|
|
36
|
-
parseMessage : async (client,topic,payload,retain)=>{
|
|
36
|
+
parseMessage : async (client,topic,payload,retain,qos=0)=>{
|
|
37
37
|
|
|
38
38
|
let topic_bck = topic;
|
|
39
39
|
let project_name = null;
|
|
@@ -77,6 +77,7 @@ var self = module.exports = {
|
|
|
77
77
|
if(device.protocol.toLowerCase() === "lwm2m"){
|
|
78
78
|
parseLwm2mMessage(client, project_name, device, topic, payload, action);
|
|
79
79
|
}else if(device.protocol.toLowerCase() === "mqtt"){
|
|
80
|
+
$.db_device.addMqttMsgLog(device.id,topic,payload,retain,qos);
|
|
80
81
|
parseMqttMessage(client, project_name, device, topic, payload, retain);
|
|
81
82
|
}
|
|
82
83
|
},
|
|
@@ -113,25 +114,40 @@ var self = module.exports = {
|
|
|
113
114
|
continue;
|
|
114
115
|
|
|
115
116
|
try{
|
|
116
|
-
const latestVersion = await $.db_firmware.getLatestVersion(model.id,release);
|
|
117
|
-
const latestAppVersion = await $.db_firmware.getLatestAppVersion(model.id,release);
|
|
118
|
-
//console.log("latestVersion:",latestVersion?.version);
|
|
119
|
-
//console.log("latestAppVersion:",latestAppVersion?.app_version);
|
|
120
|
-
|
|
121
117
|
const devices = await $.db_device.listByModel(model.id);
|
|
122
118
|
|
|
123
119
|
if(devices == null)
|
|
124
120
|
continue;
|
|
125
121
|
|
|
122
|
+
const firmwareCache = new Map();
|
|
123
|
+
|
|
126
124
|
for (const device of devices) {
|
|
127
125
|
|
|
128
126
|
if(device?.accept_release != release){
|
|
129
127
|
continue;
|
|
130
128
|
}
|
|
131
129
|
|
|
130
|
+
if(!device?.variant_id){
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const cacheKey = `${model.id}_${release}_${device.variant_id}`;
|
|
135
|
+
if(!firmwareCache.has(cacheKey)){
|
|
136
|
+
const latestVersion = await $.db_firmware.getLatestVersion(model.id,release,device.variant_id);
|
|
137
|
+
const latestAppVersion = await $.db_firmware.getLatestAppVersion(model.id,release,device.variant_id);
|
|
138
|
+
firmwareCache.set(cacheKey, { latestVersion, latestAppVersion });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { latestVersion, latestAppVersion } = firmwareCache.get(cacheKey);
|
|
142
|
+
//console.log("latestVersion:",latestVersion?.version);
|
|
143
|
+
//console.log("latestAppVersion:",latestAppVersion?.app_version);
|
|
144
|
+
|
|
145
|
+
if(!latestVersion && !latestAppVersion)
|
|
146
|
+
continue;
|
|
147
|
+
|
|
132
148
|
let obj = null;
|
|
133
149
|
// insert filename on fota table for this device or update.
|
|
134
|
-
if(device?.app_version != latestAppVersion.app_version){
|
|
150
|
+
if(latestAppVersion && device?.app_version != latestAppVersion.app_version){
|
|
135
151
|
obj = {
|
|
136
152
|
model_id : device.model_id,
|
|
137
153
|
target_version : latestVersion.version,
|
|
@@ -251,7 +267,6 @@ var self = module.exports = {
|
|
|
251
267
|
}
|
|
252
268
|
},
|
|
253
269
|
|
|
254
|
-
|
|
255
270
|
updateSensor : async (device,ref,payload)=>{
|
|
256
271
|
|
|
257
272
|
let sensors = await $.db_device.getSensorsByRef(device.id,ref)
|
|
@@ -304,17 +319,37 @@ var self = module.exports = {
|
|
|
304
319
|
|
|
305
320
|
handleMqttTopic : async(device, dbTopic, payload, set)=>{
|
|
306
321
|
if(set){
|
|
322
|
+
if(payload === "" || payload == null){
|
|
323
|
+
// acknowledgment
|
|
324
|
+
$.db_device.setSynchedTopic(dbTopic.id,true);
|
|
325
|
+
$.db_device.updateRemoteTopic(dbTopic.id,dbTopic?.localData?.value ? dbTopic.localData.value : dbTopic?.localData);
|
|
326
|
+
let from = "device";
|
|
327
|
+
let action = "ack"
|
|
328
|
+
$.db_device.addMqttLog(device.id,dbTopic.id,from,action,null);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
307
331
|
// update local topic
|
|
332
|
+
let from = "server";
|
|
333
|
+
let action = "write";
|
|
334
|
+
$.db_device.addMqttLog(device.id,dbTopic.id,from,action,JSON.stringify(payload));
|
|
335
|
+
|
|
308
336
|
$.db_device.updateLocalTopic(dbTopic.id,payload)
|
|
309
337
|
.then(()=>{
|
|
310
338
|
// set as not synched
|
|
311
|
-
|
|
339
|
+
if(isDeepStrictEqual(payload, dbTopic?.remoteData))
|
|
340
|
+
$.db_device.setSynchedTopic(dbTopic.id,true);
|
|
341
|
+
else
|
|
342
|
+
$.db_device.setSynchedTopic(dbTopic.id,false);
|
|
312
343
|
})
|
|
313
344
|
.catch((error)=>{
|
|
314
345
|
console.error(error);
|
|
315
346
|
})
|
|
316
347
|
}else{
|
|
317
348
|
// update remote topic
|
|
349
|
+
let from = "device";
|
|
350
|
+
let action = "update";
|
|
351
|
+
$.db_device.addMqttLog(device.id,dbTopic.id,from,action,JSON.stringify(payload));
|
|
352
|
+
|
|
318
353
|
$.db_device.updateRemoteTopic(dbTopic.id,payload)
|
|
319
354
|
.then(async (res)=>{
|
|
320
355
|
// check if topics mismatch
|
|
@@ -368,8 +403,17 @@ async function parseLwm2mMessage(client, project_name, device, topic, payload, a
|
|
|
368
403
|
|
|
369
404
|
async function parseMqttMessage(client, project_name, device, topic, payload, retain){
|
|
370
405
|
|
|
371
|
-
if(topic.endsWith("/get"))
|
|
406
|
+
if(topic.endsWith("/get")){
|
|
407
|
+
let findTopic = $.parser.getWordBeforeLastSlash(topic);
|
|
408
|
+
const dbTopic = await $.db_device.getMqttTopic(device.id,findTopic)
|
|
409
|
+
if(dbTopic != null){
|
|
410
|
+
let from = "server";
|
|
411
|
+
let action = "request";
|
|
412
|
+
let data = null;
|
|
413
|
+
$.db_device.addMqttLog(device.id,dbTopic.id,from,action,data);
|
|
414
|
+
}
|
|
372
415
|
return;
|
|
416
|
+
}
|
|
373
417
|
|
|
374
418
|
const topicBck = topic;
|
|
375
419
|
//console.log("[MQTT] parse topic: ",topic);
|
|
@@ -470,7 +514,8 @@ async function parseMqttMessage(client, project_name, device, topic, payload, re
|
|
|
470
514
|
}
|
|
471
515
|
}
|
|
472
516
|
break;
|
|
473
|
-
case "settings":
|
|
517
|
+
case "settings": // deprecated
|
|
518
|
+
break;
|
|
474
519
|
if(topic.endsWith("/set")){
|
|
475
520
|
updateLocalSettings(device,topic,payload);
|
|
476
521
|
}else{
|
|
@@ -510,19 +555,25 @@ async function synchMqttTopic(device,dbTopic){
|
|
|
510
555
|
let mqtt_prefix = `${project.name}/${device.uid}`;
|
|
511
556
|
let topic = `${mqtt_prefix}/${dbTopic.topic}/set`
|
|
512
557
|
let payload = "";
|
|
513
|
-
if(dbTopic?.
|
|
558
|
+
if(dbTopic?.localData && typeof dbTopic?.localData === 'object'){
|
|
514
559
|
try{
|
|
515
|
-
|
|
560
|
+
if(dbTopic?.localData?.value)
|
|
561
|
+
payload = JSON.stringify(dbTopic?.localData?.value);
|
|
562
|
+
else
|
|
563
|
+
payload = JSON.stringify(dbTopic?.localData);
|
|
516
564
|
}catch(err){
|
|
517
565
|
console.error(err);
|
|
518
566
|
return;
|
|
519
567
|
}
|
|
520
568
|
}else{
|
|
521
|
-
payload = dbTopic?.
|
|
569
|
+
payload = dbTopic?.localData;
|
|
522
570
|
}
|
|
523
571
|
$.mqtt_client.publish(topic,payload,{qos:1,retain:false});
|
|
524
572
|
}
|
|
525
573
|
|
|
574
|
+
// keep a struct with all settings configured on the device
|
|
575
|
+
// only topics <project>/<uid>/settings are stored on this struct
|
|
576
|
+
// easier way to return all settings at once
|
|
526
577
|
async function updateLocalSettings(device,topic,payload){
|
|
527
578
|
|
|
528
579
|
$.db_device.addLog(device.id,"local_settings",JSON.stringify(payload));
|
|
@@ -571,6 +622,9 @@ async function updateLocalSettings(device,topic,payload){
|
|
|
571
622
|
}
|
|
572
623
|
}
|
|
573
624
|
|
|
625
|
+
// keep a struct with all settings configured on the device
|
|
626
|
+
// only topics <project>/<uid>/settings are stored on this struct
|
|
627
|
+
// easier way to return all settings at once
|
|
574
628
|
async function updateRemoteSettings(device,topic,payload){
|
|
575
629
|
|
|
576
630
|
$.db_device.addLog(device.id,"remote_settings",JSON.stringify(payload));
|
|
@@ -614,13 +668,15 @@ async function updateRemoteSettings(device,topic,payload){
|
|
|
614
668
|
|
|
615
669
|
try {
|
|
616
670
|
await $.db_device.updateRemoteSettings(JSON.stringify(settings), device.id);
|
|
617
|
-
|
|
671
|
+
// deprecated, only defined topics with synch enabled can be synched
|
|
672
|
+
if(device?.synch && false)
|
|
618
673
|
synchSettings(device,route[0]);
|
|
619
674
|
} catch (err) {
|
|
620
675
|
console.error("Failed to update local settings:", err);
|
|
621
676
|
}
|
|
622
677
|
}
|
|
623
678
|
|
|
679
|
+
// deprecated - only defined mqtt topics can be synched
|
|
624
680
|
async function synchSettings(device,key){
|
|
625
681
|
|
|
626
682
|
console.log(`check topic ${key} from ${device.uid}`);
|
|
@@ -631,6 +687,7 @@ async function synchSettings(device,key){
|
|
|
631
687
|
console.log(remoteSettings)
|
|
632
688
|
|
|
633
689
|
//let keys = await checkSettings(localSettings,remoteSettings);
|
|
690
|
+
/*
|
|
634
691
|
if(localSettings?.hasOwnProperty(key) && remoteSettings?.hasOwnProperty(key)){
|
|
635
692
|
if(!isDeepStrictEqual(localSettings?.[key], remoteSettings?.[key])){
|
|
636
693
|
const project = await $.db_project.getById(device.project_id);
|
|
@@ -647,9 +704,9 @@ async function synchSettings(device,key){
|
|
|
647
704
|
}
|
|
648
705
|
}
|
|
649
706
|
}
|
|
707
|
+
*/
|
|
650
708
|
}
|
|
651
709
|
|
|
652
|
-
|
|
653
710
|
function handleFotaSuccess (deviceId){
|
|
654
711
|
let object = {
|
|
655
712
|
"nAttempts" : 0,
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
const parser = require('../aux/parser');
|
|
2
|
+
|
|
3
|
+
// Mock global dependencies
|
|
4
|
+
global.$ = {
|
|
5
|
+
db: {
|
|
6
|
+
connect: jest.fn(),
|
|
7
|
+
getTables: jest.fn(),
|
|
8
|
+
deleteOldEntries: jest.fn()
|
|
9
|
+
},
|
|
10
|
+
parser: parser,
|
|
11
|
+
db_project: {
|
|
12
|
+
getByName: jest.fn(),
|
|
13
|
+
insert: jest.fn(),
|
|
14
|
+
getById: jest.fn()
|
|
15
|
+
},
|
|
16
|
+
db_device: {
|
|
17
|
+
get: jest.fn(),
|
|
18
|
+
insert: jest.fn(),
|
|
19
|
+
update: jest.fn(),
|
|
20
|
+
addLog: jest.fn(),
|
|
21
|
+
getLocalSettings: jest.fn(),
|
|
22
|
+
updateLocalSettings: jest.fn(),
|
|
23
|
+
getRemoteSettings: jest.fn(),
|
|
24
|
+
updateRemoteSettings: jest.fn(),
|
|
25
|
+
listByModel: jest.fn(),
|
|
26
|
+
getById: jest.fn(),
|
|
27
|
+
getMqttTopic: jest.fn(),
|
|
28
|
+
getSensorsByRef: jest.fn(),
|
|
29
|
+
updateLocalTopic: jest.fn(),
|
|
30
|
+
setSynchedTopic: jest.fn(),
|
|
31
|
+
updateRemoteTopic: jest.fn(),
|
|
32
|
+
getAssociatedDevice: jest.fn()
|
|
33
|
+
},
|
|
34
|
+
db_model: {
|
|
35
|
+
getByName: jest.fn(),
|
|
36
|
+
getAll: jest.fn(),
|
|
37
|
+
getById: jest.fn()
|
|
38
|
+
},
|
|
39
|
+
db_sensor: {
|
|
40
|
+
getByRef: jest.fn(),
|
|
41
|
+
insert: jest.fn()
|
|
42
|
+
},
|
|
43
|
+
db_data: {
|
|
44
|
+
updateJson: jest.fn(),
|
|
45
|
+
addJsonLog: jest.fn(),
|
|
46
|
+
update: jest.fn(),
|
|
47
|
+
addLog: jest.fn(),
|
|
48
|
+
getAssociatedToDevice: jest.fn()
|
|
49
|
+
},
|
|
50
|
+
db_firmware: {
|
|
51
|
+
getLatestVersion: jest.fn(),
|
|
52
|
+
getLatestAppVersion: jest.fn(),
|
|
53
|
+
getById: jest.fn()
|
|
54
|
+
},
|
|
55
|
+
db_fota: {
|
|
56
|
+
getEntry: jest.fn(),
|
|
57
|
+
update: jest.fn(),
|
|
58
|
+
getUpdatable: jest.fn(),
|
|
59
|
+
newLog: jest.fn(),
|
|
60
|
+
updateLog: jest.fn()
|
|
61
|
+
},
|
|
62
|
+
config: {
|
|
63
|
+
web: {
|
|
64
|
+
protocol: 'https://',
|
|
65
|
+
domain: 'example.com',
|
|
66
|
+
fw_path: '/firmware/'
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
mqtt_client: {
|
|
70
|
+
publish: jest.fn()
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
global.BASE_DIR = '/mock/base/dir';
|
|
75
|
+
|
|
76
|
+
// Mock require for project modules
|
|
77
|
+
jest.mock('fs');
|
|
78
|
+
|
|
79
|
+
const device = require('./device');
|
|
80
|
+
|
|
81
|
+
describe('Device Module', () => {
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
// Clear all mocks before each test
|
|
84
|
+
jest.clearAllMocks();
|
|
85
|
+
|
|
86
|
+
// Setup default mock implementations
|
|
87
|
+
$.db.connect.mockImplementation((config, callback) => callback());
|
|
88
|
+
$.db_project.getByName.mockResolvedValue(null);
|
|
89
|
+
$.db_device.getMqttTopic.mockResolvedValue(null);
|
|
90
|
+
$.db_device.getSensorsByRef.mockResolvedValue([]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('init', () => {
|
|
94
|
+
it('should initialize database connection and projects successfully', async () => {
|
|
95
|
+
const mockConfig = { database: 'test' };
|
|
96
|
+
const mockProjects = ['project1', 'project2'];
|
|
97
|
+
|
|
98
|
+
// Mock project modules
|
|
99
|
+
const mockProject1 = { init: jest.fn() };
|
|
100
|
+
const mockProject2 = { init: jest.fn() };
|
|
101
|
+
|
|
102
|
+
jest.doMock(`${BASE_DIR}/projects/project1/project1.js`, () => mockProject1, { virtual: true });
|
|
103
|
+
jest.doMock(`${BASE_DIR}/projects/project2/project2.js`, () => mockProject2, { virtual: true });
|
|
104
|
+
|
|
105
|
+
$.db_project.getByName.mockResolvedValue(null);
|
|
106
|
+
$.db_project.insert.mockResolvedValue();
|
|
107
|
+
|
|
108
|
+
await device.init(mockConfig, mockProjects);
|
|
109
|
+
|
|
110
|
+
expect($.db.connect).toHaveBeenCalledWith(mockConfig, expect.any(Function));
|
|
111
|
+
expect($.db_project.getByName).toHaveBeenCalledTimes(2);
|
|
112
|
+
expect($.db_project.insert).toHaveBeenCalledTimes(2);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('parseMessage', () => {
|
|
117
|
+
let mockClient, mockDevice;
|
|
118
|
+
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
mockClient = { id: 'test-client' };
|
|
121
|
+
mockDevice = {
|
|
122
|
+
id: 1,
|
|
123
|
+
uid: 'test-device-123',
|
|
124
|
+
project_id: 1,
|
|
125
|
+
status: 'offline',
|
|
126
|
+
tech: 'wifi',
|
|
127
|
+
version: '1.0.0',
|
|
128
|
+
app_version: '1.0.0',
|
|
129
|
+
protocol: 'mqtt'
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
$.db_project.getByName.mockResolvedValue({
|
|
133
|
+
id: 1,
|
|
134
|
+
name: 'testproject',
|
|
135
|
+
uidPrefix: 'test-device-',
|
|
136
|
+
uidLength: 15
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
$.db_device.get.mockResolvedValue(mockDevice);
|
|
140
|
+
$.db_fota.update.mockResolvedValue();
|
|
141
|
+
$.db_fota.updateLog.mockResolvedValue();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should parse status message correctly', async () => {
|
|
145
|
+
const topic = 'testproject/test-device-123/status';
|
|
146
|
+
const payload = 'online';
|
|
147
|
+
|
|
148
|
+
await device.parseMessage(mockClient, topic, payload, false);
|
|
149
|
+
|
|
150
|
+
expect($.db_device.update).toHaveBeenCalledWith(1, 'status', 'online');
|
|
151
|
+
expect($.db_device.addLog).toHaveBeenCalledWith(1, 'status', 'online');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should publish get topics when device comes online', async () => {
|
|
155
|
+
const topic = 'testproject/test-device-123/status';
|
|
156
|
+
const payload = 'online';
|
|
157
|
+
mockDevice.remote_settings = {};
|
|
158
|
+
|
|
159
|
+
await device.parseMessage(mockClient, topic, payload, false);
|
|
160
|
+
|
|
161
|
+
expect($.mqtt_client.publish).toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should parse model message correctly when device tech differs from payload', async () => {
|
|
165
|
+
const topic = 'testproject/test-device-123/model';
|
|
166
|
+
const payload = 'TEST_MODEL';
|
|
167
|
+
|
|
168
|
+
// Make sure device.tech is different from payload so the condition passes
|
|
169
|
+
mockDevice.tech = 'different_tech';
|
|
170
|
+
$.db_device.get.mockResolvedValue(mockDevice);
|
|
171
|
+
$.db_model.getByName.mockResolvedValue({ id: 5 });
|
|
172
|
+
|
|
173
|
+
await device.parseMessage(mockClient, topic, payload, false);
|
|
174
|
+
|
|
175
|
+
expect($.db_model.getByName).toHaveBeenCalledWith('TEST_MODEL');
|
|
176
|
+
expect($.db_device.update).toHaveBeenCalledWith(1, 'model_id', 5);
|
|
177
|
+
expect($.db_device.addLog).toHaveBeenCalledWith(1, 'model_id', 5);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should parse version message correctly when device has existing version', async () => {
|
|
181
|
+
const topic = 'testproject/test-device-123/version';
|
|
182
|
+
const payload = '2.0.0';
|
|
183
|
+
|
|
184
|
+
// Device already has a version that's different from payload
|
|
185
|
+
mockDevice.version = '1.0.0';
|
|
186
|
+
$.db_device.get.mockResolvedValue(mockDevice);
|
|
187
|
+
|
|
188
|
+
await device.parseMessage(mockClient, topic, payload, false);
|
|
189
|
+
|
|
190
|
+
expect($.db_device.addLog).toHaveBeenCalledWith(1, 'version', '2.0.0');
|
|
191
|
+
expect($.db_device.update).toHaveBeenCalledWith(1, 'version', '2.0.0');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should parse app_version message correctly when device has existing app_version', async () => {
|
|
195
|
+
const topic = 'testproject/test-device-123/app_version';
|
|
196
|
+
const payload = '2.0.0';
|
|
197
|
+
|
|
198
|
+
// Device already has an app_version that's different from payload
|
|
199
|
+
mockDevice.app_version = '1.0.0';
|
|
200
|
+
$.db_device.get.mockResolvedValue(mockDevice);
|
|
201
|
+
|
|
202
|
+
await device.parseMessage(mockClient, topic, payload, false);
|
|
203
|
+
|
|
204
|
+
expect($.db_device.addLog).toHaveBeenCalledWith(1, 'app_version', '2.0.0');
|
|
205
|
+
expect($.db_device.update).toHaveBeenCalledWith(1, 'app_version', '2.0.0');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should return early for unknown project', async () => {
|
|
209
|
+
const topic = 'unknownproject/test-device-123/status';
|
|
210
|
+
const payload = 'online';
|
|
211
|
+
|
|
212
|
+
$.db_project.getByName.mockResolvedValue(null);
|
|
213
|
+
|
|
214
|
+
await device.parseMessage(mockClient, topic, payload, false);
|
|
215
|
+
|
|
216
|
+
expect($.db_device.get).not.toHaveBeenCalled();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should return early when uid does not match project prefix', async () => {
|
|
220
|
+
const topic = 'testproject/other-device-999/status';
|
|
221
|
+
const payload = 'online';
|
|
222
|
+
|
|
223
|
+
await device.parseMessage(mockClient, topic, payload, false);
|
|
224
|
+
|
|
225
|
+
expect($.db_device.get).not.toHaveBeenCalled();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should return early when device is not found', async () => {
|
|
229
|
+
const topic = 'testproject/test-device-123/status';
|
|
230
|
+
const payload = 'online';
|
|
231
|
+
|
|
232
|
+
$.db_device.get.mockResolvedValue(null);
|
|
233
|
+
|
|
234
|
+
await device.parseMessage(mockClient, topic, payload, false);
|
|
235
|
+
|
|
236
|
+
expect($.db_device.update).not.toHaveBeenCalled();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should handle settings/set messages (local settings)', async () => {
|
|
240
|
+
const settingsPayload = { ssid: 'test-network' };
|
|
241
|
+
const topic = 'testproject/test-device-123/settings/wifi/ssid/set';
|
|
242
|
+
const payload = JSON.stringify(settingsPayload);
|
|
243
|
+
|
|
244
|
+
$.db_device.getLocalSettings.mockResolvedValue({});
|
|
245
|
+
$.db_device.updateLocalSettings.mockResolvedValue();
|
|
246
|
+
|
|
247
|
+
await device.parseMessage(mockClient, topic, payload, false);
|
|
248
|
+
|
|
249
|
+
// updateLocalSettings calls addLog with the JSON-parsed (object) payload
|
|
250
|
+
expect($.db_device.addLog).toHaveBeenCalledWith(
|
|
251
|
+
1,
|
|
252
|
+
'local_settings',
|
|
253
|
+
JSON.stringify(settingsPayload)
|
|
254
|
+
);
|
|
255
|
+
expect($.db_device.updateLocalSettings).toHaveBeenCalled();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should handle settings messages without /set (remote settings)', async () => {
|
|
259
|
+
const settingsPayload = { threshold: 25 };
|
|
260
|
+
const topic = 'testproject/test-device-123/settings/sensor/temperature';
|
|
261
|
+
const payload = JSON.stringify(settingsPayload);
|
|
262
|
+
|
|
263
|
+
$.db_device.getRemoteSettings.mockResolvedValue({});
|
|
264
|
+
$.db_device.updateRemoteSettings.mockResolvedValue();
|
|
265
|
+
|
|
266
|
+
await device.parseMessage(mockClient, topic, payload, false);
|
|
267
|
+
|
|
268
|
+
expect($.db_device.addLog).toHaveBeenCalledWith(
|
|
269
|
+
1,
|
|
270
|
+
'remote_settings',
|
|
271
|
+
JSON.stringify(settingsPayload)
|
|
272
|
+
);
|
|
273
|
+
expect($.db_device.updateRemoteSettings).toHaveBeenCalled();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should handle fw messages with JSON payload', async () => {
|
|
277
|
+
const fwPayload = { version: '1.0.0', build: '123' };
|
|
278
|
+
const topic = 'testproject/test-device-123/fw';
|
|
279
|
+
const payload = JSON.stringify(fwPayload);
|
|
280
|
+
|
|
281
|
+
$.db_data.updateJson.mockResolvedValue();
|
|
282
|
+
$.db_data.addJsonLog.mockResolvedValue();
|
|
283
|
+
|
|
284
|
+
await device.parseMessage(mockClient, topic, payload, false);
|
|
285
|
+
|
|
286
|
+
expect($.db_data.updateJson).toHaveBeenCalledWith('fw', 1, fwPayload);
|
|
287
|
+
expect($.db_data.addJsonLog).toHaveBeenCalledWith('logs_fw', 1, fwPayload, '');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should handle fw/fota/update/status (FOTA error) message', async () => {
|
|
291
|
+
const topic = 'testproject/test-device-123/fw/fota/update/status';
|
|
292
|
+
const payload = 'Download failed';
|
|
293
|
+
|
|
294
|
+
await device.parseMessage(mockClient, topic, payload, false);
|
|
295
|
+
|
|
296
|
+
expect($.db_fota.updateLog).toHaveBeenCalledWith(1, { error: 'Download failed' });
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should skip topics ending with /get', async () => {
|
|
300
|
+
const topic = 'testproject/test-device-123/settings/wifi/get';
|
|
301
|
+
const payload = '';
|
|
302
|
+
|
|
303
|
+
await device.parseMessage(mockClient, topic, payload, false);
|
|
304
|
+
|
|
305
|
+
expect($.db_device.update).not.toHaveBeenCalled();
|
|
306
|
+
expect($.db_device.addLog).not.toHaveBeenCalled();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe('deleteLogs', () => {
|
|
311
|
+
it('should delete old logs from all log tables', async () => {
|
|
312
|
+
const mockTables = [
|
|
313
|
+
{ 'Tables_in_mqtt-aedes': 'logs_device' },
|
|
314
|
+
{ 'Tables_in_mqtt-aedes': 'logs_sensor' },
|
|
315
|
+
{ 'Tables_in_mqtt-aedes': 'regular_table' }
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
$.db.getTables.mockResolvedValue(mockTables);
|
|
319
|
+
$.db.deleteOldEntries.mockResolvedValue();
|
|
320
|
+
|
|
321
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
322
|
+
|
|
323
|
+
await device.deleteLogs();
|
|
324
|
+
|
|
325
|
+
expect($.db.getTables).toHaveBeenCalled();
|
|
326
|
+
expect($.db.deleteOldEntries).toHaveBeenCalledTimes(2); // Only log tables
|
|
327
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('deleting logs of table'));
|
|
328
|
+
|
|
329
|
+
consoleSpy.mockRestore();
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe('checkFota', () => {
|
|
334
|
+
const mockModels = [
|
|
335
|
+
{ id: 1, name: 'test-model' }
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
const mockDevices = [
|
|
339
|
+
{
|
|
340
|
+
id: 1,
|
|
341
|
+
uid: 'device-123',
|
|
342
|
+
model_id: 1,
|
|
343
|
+
variant_id: 2,
|
|
344
|
+
version: '1.0.0',
|
|
345
|
+
app_version: '1.0.0',
|
|
346
|
+
accept_release: 'prod'
|
|
347
|
+
}
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
beforeEach(() => {
|
|
351
|
+
$.db_model.getAll.mockResolvedValue(mockModels);
|
|
352
|
+
$.db_device.listByModel.mockResolvedValue(mockDevices);
|
|
353
|
+
$.db_fota.getEntry.mockResolvedValue(null);
|
|
354
|
+
$.db_fota.update.mockResolvedValue();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should check for firmware updates', async () => {
|
|
358
|
+
const latestVersion = { id: 1, version: '2.0.0' };
|
|
359
|
+
const latestAppVersion = { id: 2, app_version: '2.0.0' };
|
|
360
|
+
|
|
361
|
+
$.db_firmware.getLatestVersion.mockResolvedValue(latestVersion);
|
|
362
|
+
$.db_firmware.getLatestAppVersion.mockResolvedValue(latestAppVersion);
|
|
363
|
+
|
|
364
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
365
|
+
|
|
366
|
+
await device.checkFota('prod');
|
|
367
|
+
|
|
368
|
+
expect($.db_model.getAll).toHaveBeenCalled();
|
|
369
|
+
expect($.db_firmware.getLatestVersion).toHaveBeenCalledWith(1, 'prod', 2);
|
|
370
|
+
expect($.db_firmware.getLatestAppVersion).toHaveBeenCalledWith(1, 'prod', 2);
|
|
371
|
+
expect($.db_fota.update).toHaveBeenCalled();
|
|
372
|
+
|
|
373
|
+
consoleSpy.mockRestore();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should skip devices with different release acceptance', async () => {
|
|
377
|
+
const devDevices = [{ ...mockDevices[0], accept_release: 'dev' }];
|
|
378
|
+
$.db_device.listByModel.mockResolvedValue(devDevices);
|
|
379
|
+
|
|
380
|
+
$.db_firmware.getLatestVersion.mockResolvedValue({ id: 1, version: '2.0.0' });
|
|
381
|
+
$.db_firmware.getLatestAppVersion.mockResolvedValue({ id: 2, app_version: '2.0.0' });
|
|
382
|
+
|
|
383
|
+
await device.checkFota('prod');
|
|
384
|
+
|
|
385
|
+
expect($.db_fota.update).not.toHaveBeenCalled();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should skip devices without variant_id', async () => {
|
|
389
|
+
const devicesWithoutVariant = [{ ...mockDevices[0], variant_id: null }];
|
|
390
|
+
$.db_device.listByModel.mockResolvedValue(devicesWithoutVariant);
|
|
391
|
+
|
|
392
|
+
$.db_firmware.getLatestVersion.mockResolvedValue({ id: 1, version: '2.0.0' });
|
|
393
|
+
$.db_firmware.getLatestAppVersion.mockResolvedValue({ id: 2, app_version: '2.0.0' });
|
|
394
|
+
|
|
395
|
+
await device.checkFota('prod');
|
|
396
|
+
|
|
397
|
+
expect($.db_firmware.getLatestVersion).not.toHaveBeenCalled();
|
|
398
|
+
expect($.db_fota.update).not.toHaveBeenCalled();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should return early when no models are found', async () => {
|
|
402
|
+
$.db_model.getAll.mockResolvedValue([]);
|
|
403
|
+
|
|
404
|
+
await device.checkFota('prod');
|
|
405
|
+
|
|
406
|
+
expect($.db_firmware.getLatestVersion).not.toHaveBeenCalled();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should not create FOTA entry when versions already match', async () => {
|
|
410
|
+
$.db_firmware.getLatestVersion.mockResolvedValue({ id: 1, version: '1.0.0' });
|
|
411
|
+
$.db_firmware.getLatestAppVersion.mockResolvedValue({ id: 2, app_version: '1.0.0' });
|
|
412
|
+
|
|
413
|
+
await device.checkFota('prod');
|
|
414
|
+
|
|
415
|
+
expect($.db_fota.update).not.toHaveBeenCalled();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should only compare firmware with the same variant_id as the device', async () => {
|
|
419
|
+
const latestVersion = { id: 1, version: '2.0.0' };
|
|
420
|
+
const latestAppVersion = { id: 2, app_version: '2.0.0' };
|
|
421
|
+
|
|
422
|
+
$.db_firmware.getLatestVersion.mockResolvedValue(latestVersion);
|
|
423
|
+
$.db_firmware.getLatestAppVersion.mockResolvedValue(latestAppVersion);
|
|
424
|
+
|
|
425
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
426
|
+
|
|
427
|
+
await device.checkFota('prod');
|
|
428
|
+
|
|
429
|
+
expect($.db_firmware.getLatestVersion).toHaveBeenCalledWith(1, 'prod', mockDevices[0].variant_id);
|
|
430
|
+
expect($.db_firmware.getLatestAppVersion).toHaveBeenCalledWith(1, 'prod', mockDevices[0].variant_id);
|
|
431
|
+
|
|
432
|
+
consoleSpy.mockRestore();
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe('triggerFota', () => {
|
|
437
|
+
beforeEach(() => {
|
|
438
|
+
$.db_fota.getUpdatable.mockResolvedValue([]);
|
|
439
|
+
$.db_fota.update.mockResolvedValue();
|
|
440
|
+
$.db_fota.newLog.mockResolvedValue();
|
|
441
|
+
$.mqtt_client.publish.mockImplementation(() => {});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('should call getUpdatable with provided release', async () => {
|
|
445
|
+
await device.triggerFota('prod');
|
|
446
|
+
|
|
447
|
+
expect($.db_fota.getUpdatable).toHaveBeenCalledWith('prod');
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('should use dev release by default', async () => {
|
|
451
|
+
await device.triggerFota();
|
|
452
|
+
|
|
453
|
+
expect($.db_fota.getUpdatable).toHaveBeenCalledWith('dev');
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
});
|
package/src/kafka/consumer.js
CHANGED