• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wirenboard / wb-mqtt-serial / 973

13 Oct 2026 12:17PM UTC coverage: 86.78% (+1.1%) from 85.703%
973

push

github

web-flow
Apply uploaded and deleted templates to polling (#1277)

templates/Upload and templates/Delete update the configuration of the devices using the template, if possible.
device/LoadConfig reads only devices with "hw" in the template.

31091 of 80727 branches covered (38.51%)

1849 of 1928 new or added lines in 21 files covered. (95.9%)

8 existing lines in 5 files now uncovered.

22403 of 25816 relevant lines covered (86.78%)

2617.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.04
/test/rpc_templates_handler_test.cpp
1
#include <algorithm>
2
#include <filesystem>
3
#include <fstream>
4

5
#include <wblib/testing/testlog.h>
6

7
#include "rpc/rpc_config_change_handler.h"
8
#include "rpc/rpc_helpers.h"
9
#include "rpc_service_test_utils.h"
10
#include "serial_config.h"
11
#include "test_utils.h"
12

13
using WBMQTT::Testing::TLoggedFixture;
14

15
namespace
16
{
17
    const std::string USED_TYPE = "MSU34";               // present in the test config
18
    const std::string STOCK_TYPE = "TestDeviceOverride"; // present only in stock templates
19
    const std::string NEW_TYPE = "UploadedDevice";
20

21
    Json::Value MakeTemplateJson(const std::string& deviceType,
31 ✔
22
                                 const std::string& title,
23
                                 const std::string& group = std::string())
24
    {
25
        Json::Value root;
31 !
26
        root["device_type"] = deviceType;
31 !
27
        root["title"] = title;
31 !
28
        if (!group.empty()) {
31 ✔
29
            root["group"] = group;
3 !
30
        }
31
        root["device"]["name"] = title;
31 !
32
        root["device"]["id"] = "test-uploaded-device";
31 !
33
        Json::Value channel;
31 !
34
        channel["name"] = "Input 1";
31 !
35
        channel["reg_type"] = "input";
31 !
36
        channel["address"] = 0;
31 !
37
        root["device"]["channels"].append(channel);
31 !
38
        return root;
31 ✔
39
    }
31 ✔
40

41
    std::string MakeTemplateContent(const std::string& deviceType,
23 ✔
42
                                    const std::string& title,
43
                                    const std::string& group = std::string())
44
    {
45
        return SerializeJson(MakeTemplateJson(deviceType, title, group));
23 !
46
    }
47

48
    Json::Value MakeUploadRequest(const std::string& content,
36 ✔
49
                                  const std::string& filename,
50
                                  bool force = false,
51
                                  const std::string& lang = std::string())
52
    {
53
        Json::Value request;
36 ✔
54
        request["content"] = content;
36 !
55
        request["filename"] = filename;
36 !
56
        if (force) {
36 !
UNCOV
57
            request["force"] = true;
×
58
        }
59
        if (!lang.empty()) {
36 ✔
60
            request["lang"] = lang;
1 !
61
        }
62
        return request;
36 ✔
63
    }
×
64

65
    Json::Value MakeDeleteRequest(const std::string& type, bool force = false)
5 ✔
66
    {
67
        Json::Value request;
5 ✔
68
        request["type"] = type;
5 !
69
        if (force) {
5 !
UNCOV
70
            request["force"] = true;
×
71
        }
72
        return request;
5 ✔
73
    }
×
74
}
75

76
class TRPCTemplatesHandlerTest: public testing::Test
77
{
78
protected:
79
    std::filesystem::path TestDir;
80
    std::filesystem::path UserTemplatesDir;
81
    std::string ConfigPath;
82
    PTemplateMap Templates;
83
    std::unique_ptr<TSerialDriverCore> SerialDriverCore;
84
    std::unique_ptr<TRPCConfigChangeHandler> Handler;
85
    std::shared_ptr<TRPCMethods> Rpc;
86

87
    void SetUp() override
23 ✔
88
    {
89
        TestDir = std::filesystem::temp_directory_path() / "wb-mqtt-serial-user-templates-test" /
69 !
90
                  ::testing::UnitTest::GetInstance()->current_test_info()->name();
46 !
91
        UserTemplatesDir = TestDir / "templates";
23 !
92
        std::filesystem::remove_all(TestDir);
23 ✔
93
        std::filesystem::create_directories(UserTemplatesDir);
23 ✔
94
        // The handler may write the configuration
95
        ConfigPath = (TestDir / "wb-mqtt-serial.conf").string();
23 !
96
        std::filesystem::copy_file(TLoggedFixture::GetDataFilePath("configs/rpc-templates-handler-test.json"),
46 !
97
                                   ConfigPath);
23 !
98
        MakeHandler(ConfigPath);
23 ✔
99
    }
23 ✔
100

101
    //! The service with the configuration file
102
    void MakeHandler(const std::string& configPath)
24 ✔
103
    {
104
        if (Rpc) {
24 ✔
105
            Rpc->Stop();
1 ✔
106
        }
107
        Handler.reset();
24 ✔
108
        Templates.reset();
24 ✔
109
        SerialDriverCore.reset();
24 ✔
110
        SerialDriverCore = std::make_unique<TSerialDriverCore>(
24 ✔
111
            MakeUnconnectedMqttDriver(),
24 !
112
            MakeSerialDriverCoreFiles(configPath,
24 !
113
                                      TLoggedFixture::GetDataFilePath("device-templates"),
72 !
114
                                      UserTemplatesDir.string()),
48 !
115
            false);
48 !
116
        Templates = SerialDriverCore->GetTemplates();
24 !
117
        Handler.reset(new TRPCConfigChangeHandler(
24 !
118
            WBMQTT::JSON::Parse(TLoggedFixture::GetDataFilePath("../groups.json")),
72 !
119
            TLoggedFixture::GetDataFilePath("../wb-mqtt-serial-rpc-templates-upload-request.schema.json"),
72 !
120
            TLoggedFixture::GetDataFilePath("../wb-mqtt-serial-rpc-templates-delete-request.schema.json"),
72 !
121
            *SerialDriverCore));
48 !
122
        Rpc = std::make_shared<TRPCMethods>();
24 !
123
        RegisterConfigChangeRpcHandlers(*Handler, Rpc);
24 !
124
    }
24 ✔
125

126
    void TearDown() override
23 ✔
127
    {
128
        Rpc->Stop();
23 ✔
129
        Handler.reset();
23 ✔
130
        Templates.reset();
23 ✔
131
        SerialDriverCore.reset();
23 ✔
132
        std::filesystem::remove_all(TestDir);
23 ✔
133
    }
23 ✔
134

135
    //! @throws std::runtime_error with "error.data" of a refused request
136
    Json::Value Call(const std::string& method, const Json::Value& request)
41 ✔
137
    {
138
        auto answer = CallAsync(*Rpc, method, request);
41 !
139
        auto error = answer->Get();
41 !
140
        if (!error.empty()) {
41 ✔
141
            throw std::runtime_error(error);
16 !
142
        }
143
        return answer->GetResult();
50 !
144
    }
57 ✔
145

146
    Json::Value Upload(const Json::Value& request)
36 ✔
147
    {
148
        return Call("templates/Upload", request);
72 !
149
    }
150

151
    Json::Value Delete(const Json::Value& request)
5 ✔
152
    {
153
        return Call("templates/Delete", request);
10 !
154
    }
155

156
    std::vector<std::string> ListUserTemplatesDir() const
25 ✔
157
    {
158
        std::vector<std::string> res;
25 ✔
159
        for (const auto& entry: std::filesystem::directory_iterator(UserTemplatesDir)) {
38 !
160
            res.push_back(entry.path().filename().string());
13 !
161
        }
25 ✔
162
        std::sort(res.begin(), res.end());
25 !
163
        return res;
25 ✔
164
    }
×
165

166
    //! Checks {"types": [{"name": <groupName>, "types": [<type description>]}]} layout and returns type description
167
    Json::Value GetSingleType(const Json::Value& response, const std::string& groupName) const
10 ✔
168
    {
169
        EXPECT_TRUE(response["types"].isArray());
10 !
170
        EXPECT_EQ(1u, response["types"].size());
10 !
171
        EXPECT_EQ(groupName, response["types"][0]["name"].asString());
10 !
172
        EXPECT_TRUE(response["types"][0]["types"].isArray());
10 !
173
        EXPECT_EQ(1u, response["types"][0]["types"].size());
10 !
174
        return response["types"][0]["types"][0];
10 ✔
175
    }
176
};
177

178
TEST_F(TRPCTemplatesHandlerTest, UploadNewType)
8 ✔
179
{
180
    auto response = Upload(MakeUploadRequest(MakeTemplateContent(NEW_TYPE, "Uploaded device"), "uploaded-device.json"));
8 !
181

182
    EXPECT_EQ(std::vector<std::string>{"uploaded-device.json"}, ListUserTemplatesDir());
8 !
183
    // Template without a group is shown in the custom group
184
    auto typeJson = GetSingleType(response, "Custom devices");
4 !
185
    EXPECT_EQ(NEW_TYPE, typeJson["type"].asString());
2 !
186
    EXPECT_EQ("Uploaded device", typeJson["name"].asString());
2 !
187
    EXPECT_TRUE(typeJson["user-defined"].asBool());
2 !
188
    EXPECT_FALSE(typeJson["deprecated"].asBool());
2 !
189
    EXPECT_EQ("modbus", typeJson["protocol"].asString());
2 !
190

191
    // The configuration of an unused type is the current one
192
    EXPECT_EQ(WBMQTT::JSON::Parse(ConfigPath), response["config"]);
2 !
193

194
    // The type is available for config editor already
195
    auto deviceTemplate = Templates->GetTemplate(NEW_TYPE);
2 !
196
    EXPECT_TRUE(deviceTemplate->IsUserDefined());
2 !
197
    EXPECT_EQ((UserTemplatesDir / "uploaded-device.json").string(), deviceTemplate->GetFilePath());
2 !
198
}
6 !
199

200
TEST_F(TRPCTemplatesHandlerTest, UploadTypeWithParameterFwVariants)
8 ✔
201
{
202
    auto templateJson = MakeTemplateJson(NEW_TYPE, "Uploaded device");
2 !
203
    Json::Value parameter;
2 !
204
    parameter["id"] = "p1";
2 !
205
    parameter["title"] = "p1";
2 !
206
    parameter["address"] = 10;
2 !
207
    parameter["enum"].append(0);
2 !
208
    parameter["enum"].append(1);
2 !
209
    parameter["enum_titles"].append("a");
2 !
210
    parameter["enum_titles"].append("b");
2 !
211
    templateJson["device"]["parameters"].append(parameter);
2 !
212
    parameter["fw"] = "2.2.0";
2 !
213
    parameter["enum"].append(2);
2 !
214
    parameter["enum_titles"].append("c");
2 !
215
    templateJson["device"]["parameters"].append(parameter);
2 !
216

217
    auto response = Upload(MakeUploadRequest(SerializeJson(templateJson), "uploaded-device.json"));
6 !
218
    GetSingleType(response, "Custom devices");
4 !
219
    EXPECT_NO_THROW(Templates->GetTemplate(NEW_TYPE)->GetTemplate());
2 !
220
}
2 ✔
221

222
TEST_F(TRPCTemplatesHandlerTest, UploadParameterFwVariantsMismatch)
8 ✔
223
{
224
    // A schema-valid template where fw variants of a parameter differ not only in enum values
225
    // must be rejected by the same check as on template load
226
    auto templateJson = MakeTemplateJson(NEW_TYPE, "Uploaded device");
2 !
227
    Json::Value parameter;
2 !
228
    parameter["id"] = "p1";
2 !
229
    parameter["title"] = "p1";
2 !
230
    parameter["address"] = 10;
2 !
231
    parameter["scale"] = 1;
2 !
232
    parameter["enum"].append(0);
2 !
233
    parameter["enum"].append(1);
2 !
234
    parameter["enum_titles"].append("a");
2 !
235
    parameter["enum_titles"].append("b");
2 !
236
    templateJson["device"]["parameters"].append(parameter);
2 !
237
    parameter["fw"] = "2.2.0";
2 !
238
    parameter["scale"] = 2;
2 !
239
    parameter["enum"].append(2);
2 !
240
    parameter["enum_titles"].append("c");
2 !
241
    templateJson["device"]["parameters"].append(parameter);
2 !
242

243
    try {
244
        Upload(MakeUploadRequest(SerializeJson(templateJson), "uploaded-device.json"));
12 !
245
        ADD_FAILURE() << "Expect an exception";
×
246
    } catch (const std::exception& e) {
2 !
247
        EXPECT_NE(std::string(e.what()).find("Parameter \"p1\" has several declarations with different \"scale\" "
6 !
248
                                             "values (\"1\" and \"2\")."),
249
                  std::string::npos)
250
            << e.what();
2 !
251
    }
2 ✔
252
    EXPECT_TRUE(ListUserTemplatesDir().empty());
2 !
253
    EXPECT_THROW(Templates->GetTemplate(NEW_TYPE), std::out_of_range);
2 !
254
}
2 ✔
255

256
TEST_F(TRPCTemplatesHandlerTest, UploadNewTypeTranslatedGroupName)
8 ✔
257
{
258
    auto response = Upload(
259
        MakeUploadRequest(MakeTemplateContent(NEW_TYPE, "Uploaded device"), "uploaded-device.json", false, "ru"));
12 !
260
    GetSingleType(response, "Произвольные устройства");
4 !
261
}
2 ✔
262

263
TEST_F(TRPCTemplatesHandlerTest, ReUploadSameTypeSameFilename)
8 ✔
264
{
265
    Upload(MakeUploadRequest(MakeTemplateContent(NEW_TYPE, "Uploaded device"), "uploaded-device.json"));
8 !
266
    auto response =
267
        Upload(MakeUploadRequest(MakeTemplateContent(NEW_TYPE, "Uploaded device v2"), "uploaded-device.json"));
8 !
268

269
    EXPECT_EQ(std::vector<std::string>{"uploaded-device.json"}, ListUserTemplatesDir());
8 !
270
    auto typeJson = GetSingleType(response, "Custom devices");
4 !
271
    EXPECT_EQ("Uploaded device v2", typeJson["name"].asString());
2 !
272
    EXPECT_EQ("Uploaded device v2", Templates->GetTemplate(NEW_TYPE)->GetTitle());
6 !
273
}
6 !
274

275
TEST_F(TRPCTemplatesHandlerTest, ReUploadSameTypeDifferentFilename)
8 ✔
276
{
277
    Upload(MakeUploadRequest(MakeTemplateContent(NEW_TYPE, "Uploaded device"), "uploaded-device.json"));
8 !
278
    Upload(MakeUploadRequest(MakeTemplateContent(NEW_TYPE, "Uploaded device v2"), "renamed-device.json"));
8 !
279

280
    // Old file is removed, there is only one template for the type
281
    EXPECT_EQ(std::vector<std::string>{"renamed-device.json"}, ListUserTemplatesDir());
8 !
282
    auto deviceTemplate = Templates->GetTemplate(NEW_TYPE);
2 !
283
    EXPECT_EQ("Uploaded device v2", deviceTemplate->GetTitle());
6 !
284
    EXPECT_EQ((UserTemplatesDir / "renamed-device.json").string(), deviceTemplate->GetFilePath());
2 !
285
}
6 !
286

287
TEST_F(TRPCTemplatesHandlerTest, ReUploadSucceedsWhenOldFileRemovalFails)
8 ✔
288
{
289
    Upload(MakeUploadRequest(MakeTemplateContent(NEW_TYPE, "Uploaded device"), "uploaded-device.json"));
8 !
290
    auto oldPath = UserTemplatesDir / "uploaded-device.json";
2 !
291
    // Make the old file unremovable: replace it with a non-empty directory
292
    std::filesystem::remove(oldPath);
2 !
293
    std::filesystem::create_directories(oldPath / "sub");
2 !
294

295
    auto response =
296
        Upload(MakeUploadRequest(MakeTemplateContent(NEW_TYPE, "Uploaded device v2"), "renamed-device.json"));
8 !
297

298
    // Upload succeeds, the new template is active
299
    EXPECT_EQ("Uploaded device v2", GetSingleType(response, "Custom devices")["name"].asString());
6 !
300
    auto newPath = UserTemplatesDir / "renamed-device.json";
2 !
301
    EXPECT_EQ(newPath.string(), Templates->GetTemplate(NEW_TYPE)->GetFilePath());
2 !
302
    EXPECT_EQ(std::vector<std::string>({"renamed-device.json", "uploaded-device.json"}), ListUserTemplatesDir());
10 !
303
}
6 !
304

305
TEST_F(TRPCTemplatesHandlerTest, UploadSanitizesFilename)
8 ✔
306
{
307
    // Only the basename is used, the file can't be written outside of the user templates directory
308
    Upload(MakeUploadRequest(MakeTemplateContent(NEW_TYPE, "Uploaded device"), "../uploaded.json"));
8 !
309
    EXPECT_EQ(std::vector<std::string>{"uploaded.json"}, ListUserTemplatesDir());
8 !
310

311
    // ".json" suffix is added if missing (old file of the type is replaced)
312
    Upload(MakeUploadRequest(MakeTemplateContent(NEW_TYPE, "Uploaded device"), "/etc/passwd"));
8 !
313
    EXPECT_EQ(std::vector<std::string>{"passwd.json"}, ListUserTemplatesDir());
8 !
314

315
    // Characters not from [A-Za-z0-9._-] are replaced with '_'
316
    Upload(MakeUploadRequest(MakeTemplateContent(NEW_TYPE, "Uploaded device"), "we ird&:name.json"));
8 !
317
    EXPECT_EQ(std::vector<std::string>{"we_ird__name.json"}, ListUserTemplatesDir());
8 !
318
}
14 !
319

320
TEST_F(TRPCTemplatesHandlerTest, UploadInvalidFilename)
8 ✔
321
{
322
    // A filename without a usable basename is rejected, nothing is written to the user templates directory
323
    EXPECT_THROW(Upload(MakeUploadRequest(MakeTemplateContent(NEW_TYPE, "Uploaded device"), "/")), std::exception);
18 !
324
    EXPECT_TRUE(ListUserTemplatesDir().empty());
2 !
325

326
    EXPECT_THROW(Upload(MakeUploadRequest(MakeTemplateContent(NEW_TYPE, "Uploaded device"), ".json")), std::exception);
18 !
327
    EXPECT_TRUE(ListUserTemplatesDir().empty());
2 !
328
}
2 ✔
329

330
TEST_F(TRPCTemplatesHandlerTest, UploadUsedTypeWithMissingConfig)
8 ✔
331
{
332
    // A missing (or unreadable) config can't refer to the template, so nothing is protected
333
    // and "force" is not required
334
    MakeHandler((TestDir / "nonexistent-config.json").string());
2 !
335

336
    auto response = Upload(MakeUploadRequest(MakeTemplateContent(USED_TYPE, "MSU34 uploaded"), "msu34.json"));
8 !
337

338
    EXPECT_EQ(std::vector<std::string>{"msu34.json"}, ListUserTemplatesDir());
8 !
339
    auto typeJson = GetSingleType(response, "Custom devices");
4 !
340
    EXPECT_EQ(USED_TYPE, typeJson["type"].asString());
2 !
341
    EXPECT_TRUE(Templates->GetTemplate(USED_TYPE)->IsUserDefined());
2 !
342
    EXPECT_TRUE(response["config"].isNull());
2 !
343
}
6 !
344

345
TEST_F(TRPCTemplatesHandlerTest, UploadInvalidatesSchemaCache)
8 ✔
346
{
347
    auto schemaBefore = SerialDriverCore->GetDeviceConfedSchemas().GetSchema(STOCK_TYPE);
2 !
348

349
    Upload(MakeUploadRequest(MakeTemplateContent(STOCK_TYPE, "Overridden device"), "override.json"));
8 !
350

351
    auto schemaAfter = SerialDriverCore->GetDeviceConfedSchemas().GetSchema(STOCK_TYPE);
2 !
352
    EXPECT_NE(schemaBefore.get(), schemaAfter.get());
2 !
353
}
2 ✔
354

355
TEST_F(TRPCTemplatesHandlerTest, OverrideUnusedStockType)
8 ✔
356
{
357
    auto stockPath = TLoggedFixture::GetDataFilePath("device-templates/test-device-override.json");
2 !
358
    std::ifstream stockFileBefore(stockPath);
2 !
359
    std::string stockContentBefore((std::istreambuf_iterator<char>(stockFileBefore)), std::istreambuf_iterator<char>());
4 !
360

361
    auto response =
362
        Upload(MakeUploadRequest(MakeTemplateContent(STOCK_TYPE, "Overridden device", "g-io"), "override.json"));
12 !
363

364
    // Stock template file is not modified, the user defined template is active
365
    std::ifstream stockFileAfter(stockPath);
2 !
366
    std::string stockContentAfter((std::istreambuf_iterator<char>(stockFileAfter)), std::istreambuf_iterator<char>());
4 !
367
    EXPECT_EQ(stockContentBefore, stockContentAfter);
2 !
368

369
    auto typeJson = GetSingleType(response, "IO modules");
4 !
370
    EXPECT_EQ(STOCK_TYPE, typeJson["type"].asString());
2 !
371
    EXPECT_TRUE(typeJson["user-defined"].asBool());
2 !
372
    auto deviceTemplate = Templates->GetTemplate(STOCK_TYPE);
2 !
373
    EXPECT_TRUE(deviceTemplate->IsUserDefined());
2 !
374
    EXPECT_EQ("Overridden device", deviceTemplate->GetTitle());
6 !
375
}
2 ✔
376

377
TEST_F(TRPCTemplatesHandlerTest, OverrideChangesGroup)
8 ✔
378
{
379
    auto response =
380
        Upload(MakeUploadRequest(MakeTemplateContent(STOCK_TYPE, "Overridden device", "g-relay"), "override.json"));
12 !
381
    GetSingleType(response, "Relays");
4 !
382

383
    // A template without a group moves the type to the custom group
384
    response = Upload(MakeUploadRequest(MakeTemplateContent(STOCK_TYPE, "Overridden device"), "override.json"));
8 !
385
    GetSingleType(response, "Custom devices");
4 !
386
}
2 ✔
387

388
TEST_F(TRPCTemplatesHandlerTest, UploadFilenameCollisionWithOtherType)
8 ✔
389
{
390
    Upload(MakeUploadRequest(MakeTemplateContent(NEW_TYPE, "Uploaded device"), "shared.json"));
8 !
391
    Upload(MakeUploadRequest(MakeTemplateContent("AnotherUploadedDevice", "Another device"), "shared.json"));
12 !
392

393
    // The file of another type is not overwritten, a numeric suffix is added
394
    EXPECT_EQ((std::vector<std::string>{"shared-1.json", "shared.json"}), ListUserTemplatesDir());
10 !
395
    EXPECT_EQ((UserTemplatesDir / "shared.json").string(), Templates->GetTemplate(NEW_TYPE)->GetFilePath());
2 !
396
    EXPECT_EQ((UserTemplatesDir / "shared-1.json").string(),
6 !
397
              Templates->GetTemplate("AnotherUploadedDevice")->GetFilePath());
2 ✔
398
}
6 !
399

400
TEST_F(TRPCTemplatesHandlerTest, UploadInvalidContent)
8 ✔
401
{
402
    // Broken JSON
403
    EXPECT_THROW(Upload(MakeUploadRequest("{ broken", "broken.json")), std::exception);
14 !
404
    EXPECT_TRUE(ListUserTemplatesDir().empty());
2 !
405

406
    // Missing device_type
407
    Json::Value withoutType;
4 !
408
    withoutType["device"]["name"] = "No type";
2 !
409
    EXPECT_THROW(Upload(MakeUploadRequest(SerializeJson(withoutType), "no-type.json")), std::exception);
12 !
410
    EXPECT_TRUE(ListUserTemplatesDir().empty());
2 !
411

412
    // Schema violation: required "device" property is missing
413
    Json::Value withoutDevice;
4 !
414
    withoutDevice["device_type"] = NEW_TYPE;
2 !
415
    EXPECT_THROW(Upload(MakeUploadRequest(SerializeJson(withoutDevice), "no-device.json")), std::exception);
12 !
416
    EXPECT_TRUE(ListUserTemplatesDir().empty());
2 !
417
}
2 ✔
418

419
TEST_F(TRPCTemplatesHandlerTest, UploadDeprecatedTemplateIsRejected)
8 ✔
420
{
421
    // Deprecated templates skip schema validation on disk load,
422
    // so uploading them is not supported at all
423
    auto root = MakeTemplateJson("DeprecatedUploadedDevice", "Deprecated uploaded device");
6 !
424
    root["deprecated"] = true;
2 !
425

426
    EXPECT_THROW(Upload(MakeUploadRequest(SerializeJson(root), "deprecated.json")), std::exception);
12 !
427
    EXPECT_TRUE(ListUserTemplatesDir().empty());
2 !
428
    EXPECT_THROW(Templates->GetTemplate("DeprecatedUploadedDevice"), std::out_of_range);
6 !
429
}
2 ✔
430

431
TEST_F(TRPCTemplatesHandlerTest, UploadMalformedMetadata)
8 ✔
432
{
433
    // Values that break template metadata parsing must be rejected by the JSON schema
434
    // before anything is written: "device" must be an object, "group" must be a string,
435
    // the whole template must be a JSON object
436
    auto nonObjectDevice = MakeTemplateJson(NEW_TYPE, "Uploaded device");
2 !
437
    nonObjectDevice["device"] = "not-an-object-or-array";
2 !
438
    auto nonStringGroup = MakeTemplateJson(NEW_TYPE, "Uploaded device");
2 !
439
    nonStringGroup["group"]["not"] = "a string";
2 !
440

441
    for (const auto& content: {SerializeJson(nonObjectDevice), SerializeJson(nonStringGroup), std::string("[1, 2]")}) {
18 !
442
        EXPECT_THROW(Upload(MakeUploadRequest(content, "malformed.json")), std::exception);
30 !
443
        // No file and no leftover *.tmp in the user templates directory
444
        EXPECT_TRUE(ListUserTemplatesDir().empty());
6 !
445
        EXPECT_THROW(Templates->GetTemplate(NEW_TYPE), std::out_of_range);
6 !
446
    }
8 !
447

448
    // A previously uploaded good template survives a malformed re-upload with the same filename
449
    Upload(MakeUploadRequest(MakeTemplateContent(NEW_TYPE, "Uploaded device"), "uploaded-device.json"));
8 !
450
    EXPECT_THROW(Upload(MakeUploadRequest(SerializeJson(nonObjectDevice), "uploaded-device.json")), std::exception);
12 !
451

452
    EXPECT_EQ(std::vector<std::string>{"uploaded-device.json"}, ListUserTemplatesDir());
8 !
453
    auto deviceTemplate = Templates->GetTemplate(NEW_TYPE);
2 !
454
    EXPECT_EQ("Uploaded device", deviceTemplate->GetTitle());
6 !
455
    EXPECT_EQ((UserTemplatesDir / "uploaded-device.json").string(), deviceTemplate->GetFilePath());
2 !
456
}
6 !
457

458
TEST_F(TRPCTemplatesHandlerTest, UploadInvalidCondition)
8 ✔
459
{
460
    // A schema-valid template with a broken "condition" expression must be rejected
461
    // by the same checks that run on first template use, before anything is written
462
    auto root = MakeTemplateJson(NEW_TYPE, "Uploaded device");
2 !
463
    root["device"]["channels"][0]["condition"] = "a b";
2 !
464

465
    EXPECT_THROW(Upload(MakeUploadRequest(SerializeJson(root), "invalid-condition.json")), std::exception);
12 !
466
    EXPECT_TRUE(ListUserTemplatesDir().empty());
2 !
467
    EXPECT_THROW(Templates->GetTemplate(NEW_TYPE), std::out_of_range);
2 !
468
}
2 ✔
469

470
TEST_F(TRPCTemplatesHandlerTest, UploadConditionSourceParameterMismatch)
8 ✔
471
{
472
    // A schema-valid template where declarations of a parameter used in conditions
473
    // differ in register reading and conversion properties must be rejected
474
    auto root = MakeTemplateJson(NEW_TYPE, "Uploaded device");
2 !
475
    root["device"]["channels"][0]["condition"] = "mode==1";
2 !
476
    Json::Value sel;
2 !
477
    sel["id"] = "sel";
2 !
478
    sel["title"] = "sel";
2 !
479
    sel["address"] = 1;
2 !
480
    root["device"]["parameters"].append(sel);
2 !
481
    Json::Value mode;
2 !
482
    mode["id"] = "mode";
2 !
483
    mode["title"] = "mode";
2 !
484
    mode["address"] = 2;
2 !
485
    mode["scale"] = 1;
2 !
486
    mode["condition"] = "sel==0";
2 !
487
    root["device"]["parameters"].append(mode);
2 !
488
    mode["scale"] = 2;
2 !
489
    mode["condition"] = "sel==1";
2 !
490
    root["device"]["parameters"].append(mode);
2 !
491

492
    try {
493
        Upload(MakeUploadRequest(SerializeJson(root), "condition-source.json"));
12 !
494
        ADD_FAILURE() << "Expect an exception";
×
495
    } catch (const std::exception& e) {
2 !
496
        EXPECT_NE(std::string(e.what()).find("Parameter \"mode\" has several declarations with different \"scale\" "
6 !
497
                                             "values (\"1\" and \"2\")."),
498
                  std::string::npos)
499
            << e.what();
2 !
500
    }
2 ✔
501
    EXPECT_TRUE(ListUserTemplatesDir().empty());
2 !
502
    EXPECT_THROW(Templates->GetTemplate(NEW_TYPE), std::out_of_range);
2 !
503
}
2 ✔
504

505
TEST_F(TRPCTemplatesHandlerTest, UploadNumericChannelEnum)
8 ✔
506
{
507
    // Templates use numeric channel enums, the schema allows strings only,
508
    // so the values must be converted before validation, as it is done on disk load
509
    auto root = MakeTemplateJson(NEW_TYPE, "Uploaded device");
2 !
510
    auto& channel = root["device"]["channels"][0];
2 !
511
    channel["enum"].append(0);
2 !
512
    channel["enum"].append(1);
2 !
513
    channel["enum_titles"].append("off");
2 !
514
    channel["enum_titles"].append("on");
2 !
515

516
    Upload(MakeUploadRequest(SerializeJson(root), "numeric-enum.json"));
6 !
517

518
    EXPECT_EQ(std::vector<std::string>{"numeric-enum.json"}, ListUserTemplatesDir());
8 !
519
    // The file keeps the original values, the loaded template has them converted
520
    auto stored = WBMQTT::JSON::Parse((UserTemplatesDir / "numeric-enum.json").string());
2 !
521
    EXPECT_TRUE(stored["device"]["channels"][0]["enum"][0].isNumeric());
2 !
522
    auto loaded = Templates->GetTemplate(NEW_TYPE)->GetTemplate();
2 !
523
    EXPECT_EQ("0", loaded["channels"][0]["enum"][0].asString());
2 !
524
    EXPECT_EQ("1", loaded["channels"][0]["enum"][1].asString());
2 !
525
}
6 !
526

527
TEST_F(TRPCTemplatesHandlerTest, DeleteOverride)
8 ✔
528
{
529
    Upload(MakeUploadRequest(MakeTemplateContent(STOCK_TYPE, "Overridden device", "g-relay"), "override.json"));
12 !
530
    auto schemaBefore = SerialDriverCore->GetDeviceConfedSchemas().GetSchema(STOCK_TYPE);
2 !
531

532
    auto response = Delete(MakeDeleteRequest(STOCK_TYPE));
2 !
533

534
    // The type falls back to the stock template and to the stock template group
535
    EXPECT_TRUE(ListUserTemplatesDir().empty());
2 !
536
    auto typeJson = GetSingleType(response, "Custom devices");
4 !
537
    EXPECT_EQ(STOCK_TYPE, typeJson["type"].asString());
2 !
538
    EXPECT_FALSE(typeJson.isMember("user-defined"));
2 !
539
    EXPECT_FALSE(Templates->GetTemplate(STOCK_TYPE)->IsUserDefined());
2 !
540

541
    // Schema cache is invalidated
542
    auto schemaAfter = SerialDriverCore->GetDeviceConfedSchemas().GetSchema(STOCK_TYPE);
2 !
543
    EXPECT_NE(schemaBefore.get(), schemaAfter.get());
2 !
544
}
2 ✔
545

546
TEST_F(TRPCTemplatesHandlerTest, DeleteSoleUserType)
8 ✔
547
{
548
    Upload(MakeUploadRequest(MakeTemplateContent(NEW_TYPE, "Uploaded device"), "uploaded-device.json"));
8 !
549

550
    auto response = Delete(MakeDeleteRequest(NEW_TYPE));
2 !
551

552
    EXPECT_TRUE(ListUserTemplatesDir().empty());
2 !
553
    EXPECT_TRUE(response["types"].isArray());
2 !
554
    EXPECT_EQ(0u, response["types"].size());
2 !
555
    EXPECT_THROW(Templates->GetTemplate(NEW_TYPE), std::out_of_range);
2 !
556

557
    // Repeated delete fails without a crash
558
    EXPECT_THROW(Delete(MakeDeleteRequest(NEW_TYPE)), std::exception);
4 !
559
}
2 ✔
560

561
TEST_F(TRPCTemplatesHandlerTest, DeleteTypeWithoutUserTemplate)
8 ✔
562
{
563
    // Stock only type
564
    EXPECT_THROW(Delete(MakeDeleteRequest(STOCK_TYPE)), std::exception);
4 !
565
    // Unknown type
566
    EXPECT_THROW(Delete(MakeDeleteRequest("UnknownDeviceType")), std::exception);
8 !
567
}
2 ✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc