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

mendersoftware / mender / 2008906109

28 Aug 2025 12:14PM UTC coverage: 76.0%. Remained the same
2008906109

push

gitlab-ci

vpodzime
fix: Send numeric inventory values as JSON numeric values

This allows numeric filtering and comparison of the data on the
server (in the UI or via API).

Co-Authored-By: Claude <noreply@anthropic.com>

Ticket: ME-429
Changelog: Commit
Signed-off-by: pasinskim <marcin.pasinski@northern.tech>
Signed-off-by: Vratislav Podzimek <vratislav.podzimek+auto-signed@northern.tech>

15 of 15 new or added lines in 1 file covered. (100.0%)

16 existing lines in 1 file now uncovered.

7407 of 9746 relevant lines covered (76.0%)

13908.86 hits per line

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

81.82
/src/mender-update/inventory.cpp
1
// Copyright 2023 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
#include <mender-update/inventory.hpp>
16

17
#include <cmath>
18
#include <functional>
19
#include <sstream>
20
#include <string>
21

22
#include <api/api.hpp>
23
#include <api/client.hpp>
24
#include <common/common.hpp>
25
#include <client_shared/conf.hpp>
26
#include <common/error.hpp>
27
#include <common/events.hpp>
28
#include <common/http.hpp>
29
#include <client_shared/inventory_parser.hpp>
30
#include <common/io.hpp>
31
#include <common/json.hpp>
32
#include <common/log.hpp>
33

34
namespace mender {
35
namespace update {
36
namespace inventory {
37

38
using namespace std;
39

40
namespace api = mender::api;
41
namespace common = mender::common;
42
namespace conf = mender::client_shared::conf;
43
namespace error = mender::common::error;
44
namespace events = mender::common::events;
45
namespace expected = mender::common::expected;
46
namespace http = mender::common::http;
47
namespace inv_parser = mender::client_shared::inventory_parser;
48
namespace io = mender::common::io;
49
namespace json = mender::common::json;
50
namespace log = mender::common::log;
51

52
const InventoryErrorCategoryClass InventoryErrorCategory;
53

UNCOV
54
const char *InventoryErrorCategoryClass::name() const noexcept {
×
55
        return "InventoryErrorCategory";
×
56
}
57

UNCOV
58
string InventoryErrorCategoryClass::message(int code) const {
×
59
        switch (code) {
×
60
        case NoError:
UNCOV
61
                return "Success";
×
62
        case BadResponseError:
UNCOV
63
                return "Bad response error";
×
64
        }
65
        assert(false);
UNCOV
66
        return "Unknown";
×
67
}
68

UNCOV
69
error::Error MakeError(InventoryErrorCode code, const string &msg) {
×
70
        return error::Error(error_condition(code, InventoryErrorCategory), msg);
1✔
71
}
72

73
const string uri = "/api/devices/v1/inventory/device/attributes";
74

75
enum class ValueType { Integer, Float, String };
76

77
static inline ValueType GetValueType(const string &value) {
43✔
78
        if (value.empty()) {
43✔
79
                return ValueType::String;
80
        }
81

82
        // First check if it's an integer
83
        auto int_result = common::StringToLongLong(value);
42✔
84
        if (int_result) {
42✔
85
                return ValueType::Integer;
86
        }
87

88
        // Then check if it's a floating point number
89
        auto double_result = common::StringToDouble(value);
34✔
90
        if (double_result && std::isfinite(double_result.value())) {
34✔
91
                return ValueType::Float;
4✔
92
        }
93

94
        return ValueType::String;
95
}
96

97
error::Error PushInventoryData(
7✔
98
        const string &inventory_generators_dir,
99
        events::EventLoop &loop,
100
        api::Client &client,
101
        size_t &last_data_hash,
102
        APIResponseHandler api_handler) {
103
        auto ex_inv_data = inv_parser::GetInventoryData(inventory_generators_dir);
7✔
104
        if (!ex_inv_data) {
7✔
UNCOV
105
                return ex_inv_data.error();
×
106
        }
107
        auto &inv_data = ex_inv_data.value();
7✔
108

109
        if (inv_data.count("mender_client_version") != 0) {
14✔
110
                inv_data["mender_client_version"].push_back(conf::kMenderVersion);
3✔
111
        } else {
112
                inv_data["mender_client_version"] = {conf::kMenderVersion};
24✔
113
        }
114

115
        stringstream top_ss;
14✔
116
        top_ss << "[";
7✔
117
        auto key_vector = common::GetMapKeyVector(inv_data);
14✔
118
        std::sort(key_vector.begin(), key_vector.end());
7✔
119
        for (const auto &key : key_vector) {
43✔
120
                top_ss << R"({"name":")";
36✔
121
                top_ss << json::EscapeString(key);
36✔
122
                top_ss << R"(","value":)";
36✔
123
                if (inv_data[key].size() == 1) {
36✔
124
                        const string &value = inv_data[key][0];
125
                        ValueType value_type = GetValueType(value);
31✔
126

127
                        if (value_type == ValueType::Integer || value_type == ValueType::Float) {
31✔
128
                                // Serialize numeric values without quotes
129
                                top_ss << value;
9✔
130
                        } else {
131
                                // Serialize string values with quotes and escaping
132
                                top_ss << "\"" << json::EscapeString(value) << "\"";
44✔
133
                        }
134
                } else {
135
                        stringstream items_ss;
10✔
136
                        items_ss << "[";
5✔
137
                        for (const auto &str : inv_data[key]) {
17✔
138
                                ValueType value_type = GetValueType(str);
12✔
139

140
                                if (value_type == ValueType::Integer || value_type == ValueType::Float) {
12✔
141
                                        // Serialize numeric values without quotes
142
                                        items_ss << str << ",";
3✔
143
                                } else {
144
                                        // Serialize string values with quotes and escaping
145
                                        items_ss << "\"" << json::EscapeString(str) << "\",";
18✔
146
                                }
147
                        }
148
                        auto items_str = items_ss.str();
149
                        // replace the trailing comma with the closing square bracket
150
                        items_str[items_str.size() - 1] = ']';
5✔
151
                        top_ss << items_str;
5✔
152
                }
153
                top_ss << R"(},)";
36✔
154
        }
155
        auto payload = top_ss.str();
156
        if (payload[payload.size() - 1] == ',') {
7✔
157
                // replace the trailing comma with the closing square bracket
158
                payload.pop_back();
7✔
159
        }
160
        payload.push_back(']');
7✔
161

162
        size_t payload_hash = std::hash<string> {}(payload);
7✔
163
        if (payload_hash == last_data_hash) {
7✔
164
                log::Info("Inventory data unchanged, not submitting");
2✔
165
                loop.Post([api_handler]() { api_handler(error::NoError); });
8✔
166
                return error::NoError;
1✔
167
        }
168

169
        http::BodyGenerator payload_gen = [payload]() {
48✔
170
                return make_shared<io::StringReader>(payload);
6✔
171
        };
6✔
172

173
        auto req = make_shared<api::APIRequest>();
6✔
174
        req->SetPath(uri);
175
        req->SetMethod(http::Method::PUT);
6✔
176
        req->SetHeader("Content-Type", "application/json");
12✔
177
        req->SetHeader("Content-Length", to_string(payload.size()));
12✔
178
        req->SetHeader("Accept", "application/json");
12✔
179
        req->SetBodyGenerator(payload_gen);
6✔
180

181
        auto received_body = make_shared<vector<uint8_t>>();
6✔
182
        return client.AsyncCall(
183
                req,
184
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
6✔
185
                        if (!exp_resp) {
6✔
UNCOV
186
                                log::Error("Request to push inventory data failed: " + exp_resp.error().message);
×
UNCOV
187
                                api_handler(exp_resp.error());
×
UNCOV
188
                                return;
×
189
                        }
190

191
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
6✔
192
                        auto resp = exp_resp.value();
6✔
193
                        auto content_length = resp->GetHeader("Content-Length");
12✔
194
                        auto ex_len = common::StringTo<size_t>(content_length.value());
6✔
195
                        if (!ex_len) {
6✔
UNCOV
196
                                log::Error("Failed to get content length from the inventory API response headers");
×
UNCOV
197
                                body_writer->SetUnlimited(true);
×
198
                        } else {
199
                                received_body->resize(ex_len.value());
6✔
200
                        }
201
                        resp->SetBodyWriter(body_writer);
12✔
202
                },
203
                [received_body, api_handler, payload_hash, &last_data_hash](
6✔
204
                        http::ExpectedIncomingResponsePtr exp_resp) {
5✔
205
                        if (!exp_resp) {
6✔
UNCOV
206
                                log::Error("Request to push inventory data failed: " + exp_resp.error().message);
×
UNCOV
207
                                api_handler(exp_resp.error());
×
UNCOV
208
                                return;
×
209
                        }
210

211
                        auto resp = exp_resp.value();
6✔
212
                        auto status = resp->GetStatusCode();
6✔
213
                        if (status == http::StatusOK) {
6✔
214
                                log::Info("Inventory data submitted successfully");
10✔
215
                                last_data_hash = payload_hash;
5✔
216
                                api_handler(error::NoError);
10✔
217
                        } else {
218
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
219
                                string err_str;
220
                                if (ex_err_msg) {
1✔
221
                                        err_str = ex_err_msg.value();
1✔
222
                                } else {
UNCOV
223
                                        err_str = resp->GetStatusMessage();
×
224
                                }
225
                                api_handler(MakeError(
2✔
226
                                        BadResponseError,
227
                                        "Got unexpected response " + to_string(status)
2✔
228
                                                + " from inventory API: " + err_str));
2✔
229
                        }
230
                });
24✔
231
}
232

233
} // namespace inventory
234
} // namespace update
235
} // namespace mender
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc