userver: /data/code/userver/libraries/protobuf/tests/json/field_mask_to_json_test.cpp Source File
Loading...
Searching...
No Matches
field_mask_to_json_test.cpp
1#include <gtest/gtest.h>
2
3#include <algorithm>
4#include <memory>
5#include <ostream>
6#include <string>
7#include <vector>
8
9#include <fmt/format.h>
10#include <google/protobuf/dynamic_message.h>
11
12#include <userver/protobuf/json/convert.hpp>
13#include <userver/utest/assert_macros.hpp>
14
15#include "utils.hpp"
16
17USERVER_NAMESPACE_BEGIN
18
19namespace protobuf::json::tests {
20
21struct FieldMaskToJsonSuccessTestParam {
22 FieldMaskMessageData input = {};
23 std::string expected_json = {};
24 PrintOptions options = {};
25
26 // This variable is used to disable some checks that fail in the native protobuf implementation.
27 bool skip_native_check = false;
28};
29
30struct FieldMaskToJsonFailureTestParam {
31 FieldMaskMessageData input = {};
32 PrintErrorCode expected_errc = {};
33 std::string expected_path = {};
34 PrintOptions options = {};
35};
36
37std::vector<std::string> ParseFieldMaskStr(std::string_view paths) {
38 std::vector<std::string> result;
39 std::size_t pos = 0;
40
41 while (true) {
42 auto comma_pos = paths.find(',', pos);
43
44 if (comma_pos == std::string_view::npos) {
45 result.emplace_back(paths.substr(pos));
46 break;
47 }
48
49 result.emplace_back(paths.substr(pos, comma_pos - pos));
50 pos = comma_pos + 1;
51 }
52
53 return result;
54}
55
56namespace {
57// fmt::format with fmt/ranges.h cannot be used here: on fmt 8.x (CI sandbox,
58// Ubuntu 22.04) fmt/ranges.h calls format_to unqualified inside fmt::detail,
59// which causes an ADL ambiguity with std::format_to from libstdc++13 that
60// clang-16 rejects as a hard error. Fixed in fmt 9.x (devcontainer), but CI
61// still runs 8.1.1.
62std::string FmtFormatCrutch(const std::optional<std::vector<std::string>>& items) {
63 if (!items) {
64 return "nullopt";
65 }
66 constexpr std::string_view sep = ", ";
67 std::string result = "[";
68 for (const auto& s : *items) {
69 result += '"';
70 result += s;
71 result += '"';
72 result += sep;
73 }
74 if (!items->empty()) {
75 result.resize(result.size() - sep.size());
76 }
77 result += ']';
78 return result;
79}
80} // namespace
81
82void PrintTo(const FieldMaskToJsonSuccessTestParam& param, std::ostream* os) {
83 *os << "{ input = {.field1=" << FmtFormatCrutch(param.input.field1) << "} }";
84}
85
86void PrintTo(const FieldMaskToJsonFailureTestParam& param, std::ostream* os) {
87 *os << "{ input = {.field1=" << FmtFormatCrutch(param.input.field1) << "} }";
88}
89
90class FieldMaskToJsonSuccessTest : public ::testing::TestWithParam<FieldMaskToJsonSuccessTestParam> {};
91class FieldMaskToJsonFailureTest : public ::testing::TestWithParam<FieldMaskToJsonFailureTestParam> {};
92
93INSTANTIATE_TEST_SUITE_P(
94 ,
95 FieldMaskToJsonSuccessTest,
96 ::testing::Values(
97 FieldMaskToJsonSuccessTestParam{FieldMaskMessageData{}, R"({})"},
98 FieldMaskToJsonSuccessTestParam{FieldMaskMessageData{std::vector<std::string>{}}, R"({"field1":""})"},
99 FieldMaskToJsonSuccessTestParam{FieldMaskMessageData{std::vector<std::string>{""}}, R"({"field1":""})"},
100 FieldMaskToJsonSuccessTestParam{
101 FieldMaskMessageData{std::vector<std::string>{"", "", "", "aaa", ""}},
102 R"({"field1":",,,aaa,"})",
103 {},
104 !kIsModernProtoJson
105 },
106 FieldMaskToJsonSuccessTestParam{
107 FieldMaskMessageData{std::vector<std::string>{"some_field"}},
108 R"({"field1":"someField"})",
109 // Does not affect field mask serialization!
110 {.preserve_proto_field_names = true}
111 },
112 FieldMaskToJsonSuccessTestParam{
113 FieldMaskMessageData{std::vector<std::string>{"some_field.another_field..one_more", "_a_b0_c"}},
114 R"({"field1":"someField.anotherField..oneMore,AB0C"})",
115 {},
116 !kIsModernProtoJson
117 }
118 )
119);
120
121// Protobuf ProtoJSON in legacy mode does not return error on invalid characters, which we
122// want to prohibit (because we do not want our clients to use syntax that may break in the
123// newer protobuf versions).
124
125INSTANTIATE_TEST_SUITE_P(
126 ,
127 FieldMaskToJsonFailureTest,
128 ::testing::Values(
129 FieldMaskToJsonFailureTestParam{
130 FieldMaskMessageData{std::vector<std::string>{"Some_field"}},
131 PrintErrorCode::kInvalidValue,
132 "field1"
133 },
134 FieldMaskToJsonFailureTestParam{
135 FieldMaskMessageData{std::vector<std::string>{"some_Field"}},
136 PrintErrorCode::kInvalidValue,
137 "field1"
138 },
139 FieldMaskToJsonFailureTestParam{
140 FieldMaskMessageData{std::vector<std::string>{"some_fielD"}},
141 PrintErrorCode::kInvalidValue,
142 "field1"
143 },
144 FieldMaskToJsonFailureTestParam{
145 FieldMaskMessageData{std::vector<std::string>{"some_f!ield"}},
146 PrintErrorCode::kInvalidValue,
147 "field1"
148 },
149 FieldMaskToJsonFailureTestParam{
150 FieldMaskMessageData{std::vector<std::string>{"__some_field"}},
151 PrintErrorCode::kInvalidValue,
152 "field1"
153 },
154 FieldMaskToJsonFailureTestParam{
155 FieldMaskMessageData{std::vector<std::string>{"some__field"}},
156 PrintErrorCode::kInvalidValue,
157 "field1"
158 },
159 FieldMaskToJsonFailureTestParam{
160 FieldMaskMessageData{std::vector<std::string>{"some_field_"}},
161 PrintErrorCode::kInvalidValue,
162 "field1"
163 },
164 FieldMaskToJsonFailureTestParam{
165 FieldMaskMessageData{std::vector<std::string>{"some_0field"}},
166 PrintErrorCode::kInvalidValue,
167 "field1"
168 }
169 )
170);
171
172TEST_P(FieldMaskToJsonSuccessTest, Test) {
173 const auto& param = GetParam();
174
175 auto input = PrepareTestData(param.input);
176 formats::json::Value json;
177 formats::json::Value expected_json;
178
179 UASSERT_NO_THROW((json = MessageToJson(input, param.options)));
180 UASSERT_NO_THROW((expected_json = PrepareJsonTestData(param.expected_json)));
181
182 if (!json.HasMember("field1")) {
183 EXPECT_FALSE(expected_json.HasMember("field1"));
184 return;
185 }
186
187 ASSERT_TRUE(json["field1"].IsString());
188 ASSERT_TRUE(expected_json["field1"].IsString());
189
190 auto json_paths = ParseFieldMaskStr(json["field1"].As<std::string>());
191 auto expected_json_paths = ParseFieldMaskStr(expected_json["field1"].As<std::string>());
192
193 std::ranges::sort(json_paths);
194 std::ranges::sort(expected_json_paths);
195
196 EXPECT_EQ(json_paths, expected_json_paths);
197
198 if (!param.skip_native_check) {
199 formats::json::Value sample_json;
200
201 UASSERT_NO_THROW((sample_json = CreateSampleJson(input, param.options)));
202
203 if (!json.HasMember("field1")) {
204 EXPECT_FALSE(sample_json.HasMember("field1"));
205 return;
206 }
207
208 ASSERT_TRUE(sample_json["field1"].IsString());
209
210 auto sample_json_paths = ParseFieldMaskStr(sample_json["field1"].As<std::string>());
211 std::ranges::sort(sample_json_paths);
212
213 EXPECT_EQ(expected_json_paths, sample_json_paths);
214 }
215}
216
217TEST_P(FieldMaskToJsonFailureTest, Test) {
218 const auto& param = GetParam();
219 auto input = PrepareTestData(param.input);
220
221 EXPECT_PRINT_ERROR((void)MessageToJson(input, param.options), param.expected_errc, param.expected_path);
222}
223
224TEST(FieldMaskToJsonAdditionalTest, InlinedNonNull) {
225 FieldMaskMessageData data{std::vector<std::string>{"some_field.nested_field"}};
226 auto message = PrepareTestData(data);
227 formats::json::Value json;
228 formats::json::Value sample;
229
230 UASSERT_NO_THROW((json = MessageToJson(message.field1(), {})));
231 UASSERT_NO_THROW((sample = CreateSampleJson(message.field1())));
232 ASSERT_TRUE(json.IsString());
233 ASSERT_TRUE(sample.IsString());
234
235 auto paths = ParseFieldMaskStr(json.As<std::string>());
236 auto sample_paths = ParseFieldMaskStr(sample.As<std::string>());
237
238 std::ranges::sort(paths);
239 std::ranges::sort(sample_paths);
240
241 EXPECT_EQ(paths, sample_paths);
242 EXPECT_EQ(paths, std::vector<std::string>{"someField.nestedField"});
243}
244
245TEST(FieldMaskToJsonAdditionalTest, InlinedNull) {
247 auto message = PrepareTestData(data);
248 formats::json::Value json;
249 formats::json::Value sample;
250
251 UASSERT_NO_THROW((json = MessageToJson(message.field1(), {})));
252 UASSERT_NO_THROW((sample = CreateSampleJson(message.field1())));
253 ASSERT_TRUE(json.IsString());
254 ASSERT_TRUE(sample.IsString());
255 EXPECT_TRUE(json.As<std::string>().empty());
256 EXPECT_TRUE(sample.As<std::string>().empty());
257}
258
259TEST(FieldMaskToJsonAdditionalTest, DynamicMessage) {
260 using Message = ::google::protobuf::FieldMask;
261 ::google::protobuf::DynamicMessageFactory factory;
262
263 {
264 std::unique_ptr<::google::protobuf::Message> message(factory.GetPrototype(Message::descriptor())->New());
265 const auto reflection = message->GetReflection();
266 const auto paths_desc = message->GetDescriptor()->FindFieldByName("paths");
267
268 reflection->AddString(message.get(), paths_desc, "some_field.another_field");
269 reflection->AddString(message.get(), paths_desc, "one_more");
270
271 formats::json::Value json;
272
273 UASSERT_NO_THROW((json = MessageToJson(*message, {})));
274 ASSERT_TRUE(json.IsString());
275
276 auto paths = ParseFieldMaskStr(json.As<std::string>());
277 std::ranges::sort(paths);
278
279 ASSERT_EQ(paths.size(), std::size_t{2});
280 EXPECT_EQ(paths[0], "oneMore");
281 EXPECT_EQ(paths[1], "someField.anotherField");
282 }
283}
284
285} // namespace protobuf::json::tests
286
287USERVER_NAMESPACE_END