// Copyright (c) 2020-2022. NetFoundry Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include "catch2_includes.hpp" #include #include #include #include #include #define BAR_MODEL(xx, ...)\ xx(num, model_number, none, num, __VA_ARGS__)\ xx(nump, model_number, ptr, nump, __VA_ARGS__) \ xx(isOK, model_bool, none, ok, __VA_ARGS__)\ xx(msg, model_string, none, msg, __VA_ARGS__)\ xx(ts, timestamp, ptr, time, __VA_ARGS__)\ xx(errors, model_string, array, errors, __VA_ARGS__)\ xx(codes, model_number, array, codes, __VA_ARGS__)\ xx(shoes, model_string, list, shoes, __VA_ARGS__) DECLARE_MODEL(Bar, BAR_MODEL) IMPL_MODEL(Bar, BAR_MODEL) #define FOO_MODEL(xx, ...) \ xx(bar, Bar, none, bar, __VA_ARGS__) \ xx(barp, Bar, ptr, barp, __VA_ARGS__) \ xx(bar_arr, Bar, array, bara, __VA_ARGS__) DECLARE_MODEL(Foo, FOO_MODEL) IMPL_MODEL(Foo, FOO_MODEL) using namespace Catch::Matchers; #define BAR1 "{\ \"num\":42,\ \"ok\": false,\ \"time\": \"2020-07-20T14:14:14.666666Z\",\ \"msg\":\"this is a message\",\ \"errors\": [\"error1\", \"error2\"], \ \"codes\": [401, 403], \ \"shoes\": [ \"sandals\", \"slippers\", \"boots\" ] \ }" #define BAR_WITH_NULL_CODES_ARRAY "{\ \"num\":42,\ \"ok\": false,\ \"time\": \"2020-07-20T14:14:14.666666Z\",\ \"msg\":\"this is a message\",\ \"errors\": [\"error1\", \"error2\"], \ \"codes\": null, \ \"shoes\": null \ }" #define BAR_WITH_MISSING_CODES_ARRAY "{\ \"num\":42,\ \"ok\": false,\ \"time\": \"2020-07-20T14:14:14.666666Z\",\ \"msg\":\"this is a message\",\ \"errors\": [\"error1\", \"error2\"] \ }" static inline std::string remove_space(const char* s) { std::string r(s); auto end = std::remove_if(r.begin(), r.end(), isspace); r.erase(end, r.end()); return r; } static void checkBar1(const Bar &bar) { CHECK(bar.num == 42); CHECK(!bar.isOK); CHECK_THAT(bar.errors[0], Equals("error1")); CHECK_THAT(bar.errors[1], Equals("error2")); CHECK(bar.errors[2] == nullptr); int c1 = *bar.codes[0]; CHECK(c1 == 401); int c2 = *bar.codes[1]; CHECK(c2 == 403); CHECK(bar.codes[2] == nullptr); } #define BAR2 "{\ \"num\":-42,\ \"ok\": true,\ }" static void checkBar2(const Bar &bar) { CHECK(bar.num == -42); CHECK(bar.isOK); CHECK(bar.errors == nullptr); CHECK(bar.codes == nullptr); } TEST_CASE("new model tests", "[model]") { const char *bar_json = BAR1; Bar bar; REQUIRE(parse_Bar(&bar, bar_json, strlen(bar_json)) == strlen(bar_json)); checkBar1(bar); auto json = Bar_to_json(&bar, 0, nullptr); CHECK(json != nullptr); std::cout << json << std::endl; free(json); free_Bar(&bar); auto j = json_tokener_parse(bar_json); REQUIRE(Bar_from_json(&bar, j) == 0); checkBar1(bar); json_object_put(j); json = Bar_to_json(&bar, 0, nullptr); std::cout << json << std::endl; free(json); free_Bar(&bar); } TEST_CASE("parse null", "[model]") { const char *json = "{" "\"barp\":null," "\"bara\":null" "}"; Foo foo; REQUIRE(parse_Foo(&foo, json, strlen(json)) == strlen(json)); REQUIRE(foo.barp == nullptr); REQUIRE(foo.bar_arr == nullptr); char *json1 = Foo_to_json(&foo, 0, NULL); std::cout << json1 << std::endl; free(json1); free_Foo(&foo); } TEST_CASE("parse null in the middle", "[model]") { const char *json = "{" "\"bar\":" BAR1 "," "\"barp\": null," "\"bara\": [" BAR1 "," BAR2 "]" "}"; Foo foo; REQUIRE(parse_Foo(&foo, json, strlen(json)) == strlen(json)); checkBar1(foo.bar); REQUIRE(foo.barp == nullptr); REQUIRE(foo.bar_arr != nullptr); CHECK(foo.bar_arr[2] == nullptr); checkBar1(*foo.bar_arr[0]); checkBar2(*foo.bar_arr[1]); char *json1 = Foo_to_json(&foo, 0, NULL); std::cout << json1 << std::endl; free(json1); free_Foo(&foo); } TEST_CASE("embedded struct", "[model]") { const char *json = "{" "\"bar\":" BAR1 "," "\"barp\": " BAR2 "," "\"bara\": [" BAR1 "," BAR2 "]" "}"; Foo foo; REQUIRE(parse_Foo(&foo, json, strlen(json)) == strlen(json)); checkBar1(foo.bar); REQUIRE(foo.barp != nullptr); checkBar2(*foo.barp); REQUIRE(foo.bar_arr != nullptr); CHECK(foo.bar_arr[2] == nullptr); checkBar1(*foo.bar_arr[0]); checkBar2(*foo.bar_arr[1]); char *json1 = Foo_to_json(&foo, 0, NULL); std::cout << json1 << std::endl; free(json1); free_Foo(&foo); } TEST_CASE("test skipped fields", "[model]") { const char *json = R"({ "bar":{ "num":42, "ok":true, "msg":"hello\nworld!" }, "skipper": [{"this":"should be skipped"},42,null], "also-skip": {"more":"skipping"}, "barp":{ "skip-field":{}, "nump":42, "ok":true, "msg":"hello world!" } })"; Foo foo; REQUIRE(parse_Foo(&foo, json, strlen(json)) == strlen(json)); REQUIRE(foo.bar.num == 42); REQUIRE(foo.bar.isOK); REQUIRE_THAT(foo.bar.msg, Catch::Matchers::Equals("hello\nworld!")); REQUIRE(foo.barp != nullptr); REQUIRE(*foo.barp->nump == 42); REQUIRE(foo.barp->isOK); REQUIRE_THAT(foo.barp->msg, Catch::Matchers::Equals("hello world!")); size_t jsonlen; char *json1 = Foo_to_json(&foo, 0, &jsonlen); std::cout << json1 << std::endl; free(json1); free_Foo(&foo); } TEST_CASE("test string escape", "[model]") { const char *json = R"({ "msg":"\thello\n\"world\"!" })"; Bar bar; REQUIRE(parse_Bar(&bar, json, strlen(json)) == strlen(json)); REQUIRE_THAT(bar.msg, Equals("\thello\n\"world\"!")); char *jsonout = Bar_to_json(&bar, 0, NULL); std::cout << jsonout << std::endl; free(jsonout); free_Bar(&bar); } TEST_CASE("parse array", "[model]") { const char *json = R"([{ "msg":"\thello\n\"world\"!" }, {"msg":"Hello again!"}])"; Bar_array bars = nullptr; int rc = parse_Bar_array(&bars, json, strlen(json)); CHECK(rc == strlen(json)); CHECK_THAT(bars[0]->msg, Catch::Matchers::Matches("\thello\n\"world\"!")); CHECK_THAT(bars[1]->msg, Catch::Matchers::Matches("Hello again!")); free_Bar_array(&bars); } TEST_CASE("parse bad array", "[model]") { const char *json = R"([{ "msg":"\thello\n\"world\"!" }, {"msg":56}])"; Bar_array bars = nullptr; int rc = parse_Bar_array(&bars, json, strlen(json)); CHECK(rc == -1); CHECK(bars == nullptr); } TEST_CASE("parse 2 objects in string ", "[model]") { const char *json = R"({ "msg":"\thello\n\"world\"!" }{"msg":"Hello again!"})"; Bar bar = {0}; int rc = parse_Bar(&bar, json, strlen(json)); REQUIRE(rc > 0); CHECK_THAT(bar.msg, Catch::Matchers::Matches("\thello\n\"world\"!")); free_Bar(&bar); rc = parse_Bar(&bar, json + rc, strlen(json) - rc); REQUIRE(rc > 0); CHECK_THAT(bar.msg, Catch::Matchers::Matches("Hello again!")); free_Bar(&bar); } TEST_CASE("parse incomplete", "[model]") { const char *json = R"({ "msg":"\thello\n\"world\"!" )"; Bar bar = {0}; REQUIRE(parse_Bar(&bar, json, strlen(json)) == MODEL_PARSE_PARTIAL); free_Bar(&bar); const char *json_array_partial = R"([ {"msg":"\thello\n\"world\"!"}, {"msg":"56")"; Bar_array bars = nullptr; int rc = parse_Bar_array(&bars, json_array_partial, strlen(json_array_partial)); free_Bar_array(&bars); CHECK(rc == MODEL_PARSE_PARTIAL); const char *json_array_invalid = R"([ {"msg":"\thello\n\"world\"!"}, {"msg":"56" ])"; rc = parse_Bar_array(&bars, json_array_invalid, strlen(json_array_invalid)); free_Bar_array(&bars); CHECK(rc == MODEL_PARSE_INVALID); } #define baz_model(XX, ...) \ XX(bar, json, none, bar, __VA_ARGS__) \ XX(ok, model_bool, none, ok, __VA_ARGS__) #undef MODEL_API #define MODEL_API static DECLARE_MODEL(Baz, baz_model) IMPL_MODEL(Baz, baz_model) TEST_CASE("test raw json", "[model]") { const char *json = "{" "\"bar\":" BAR1 "," "\"ok\": true" "}"; Baz baz; REQUIRE(parse_Baz(&baz, json, strlen(json)) == strlen(json)); REQUIRE_THAT(remove_space(baz.bar), Equals(remove_space(BAR1))); REQUIRE(baz.ok); Bar bar; REQUIRE(parse_Bar(&bar, baz.bar, strlen(baz.bar)) == strlen(baz.bar)); checkBar1(bar); free_Bar(&bar); free_Baz(&baz); } #define map_model(XX, ...) \ XX(map, json, map, map, __VA_ARGS__) \ XX(ok, model_bool, none, ok, __VA_ARGS__) DECLARE_MODEL(ObjMap, map_model) IMPL_MODEL(ObjMap, map_model) TEST_CASE("model map test", "[model]") { const char *json = "{" "\"map\":" BAR1 "," "\"ok\": true" "}"; ObjMap o; REQUIRE(parse_ObjMap(&o, json, strlen(json)) == strlen(json)); CHECK(o.ok); CHECK_THAT((const char *) model_map_get(&o.map, "num"), Equals("42")); CHECK_THAT( remove_space((const char *) model_map_get(&o.map, "errors")), Equals(R"(["error1","error2"])")); char *j = ObjMap_to_json(&o, 0, NULL); std::cout << j << std::endl; free(j); free_ObjMap(&o); } TEST_CASE("model compare", "[model]") { Bar b1; memset(&b1, 0, sizeof(Bar)); b1.num = 45; b1.isOK = false; b1.msg = (char *) "this is bar1"; Bar b2; memset(&b2, 0, sizeof(Bar)); b2.num = 42; b2.isOK = true; b2.msg = (char *) "this is bar2"; CHECK(model_cmp(&b1, &b2, &Bar_META) != 0); b1.isOK = true; CHECK(model_cmp(&b1, &b2, &Bar_META) != 0); b2.num = 45; b2.msg = (char *) "this is bar1"; CHECK(model_cmp(&b1, &b2, &Bar_META) == 0); b2.msg = nullptr; CHECK(model_cmp(&b1, &b2, &Bar_META) != 0); b1.msg = nullptr; CHECK(model_cmp(&b1, &b2, &Bar_META) == 0); } TEST_CASE("model compare with map", "[model]") { ObjMap o1; o1.map = {0}; o1.ok = true; ObjMap o2; o2.map = {0}, o2.ok = true; CHECK(cmp_ObjMap(&o1, &o2) == 0); model_map_set(&o1.map, "key1", (void *) "one"); CHECK(cmp_ObjMap(&o1, &o2) != 0); model_map_set(&o2.map, "key2", (void *) "two"); CHECK(cmp_ObjMap(&o1, &o2) != 0); model_map_set(&o2.map, "key1", (void *) "one"); model_map_set(&o1.map, "key2", (void *) "two"); CHECK(cmp_ObjMap(&o1, &o2) == 0); model_map_iter it = model_map_iterator(&o1.map); while (it != nullptr) { it = model_map_it_remove(it); } it = model_map_iterator(&o2.map); while (it != nullptr) { it = model_map_it_remove(it); } free_ObjMap(&o1); free_ObjMap(&o2); } TEST_CASE("model compare with array", "[model]") { const char *bar_json = BAR1; Bar bar1, bar2; REQUIRE(parse_Bar(&bar1, bar_json, strlen(bar_json)) == strlen(bar_json)); REQUIRE(parse_Bar(&bar2, bar_json, strlen(bar_json)) == strlen(bar_json)); CHECK(cmp_Bar(&bar1, &bar2) == 0); free((char*)bar1.errors[0]); bar1.errors[0] = strdup("changed error"); CHECK(cmp_Bar(&bar1, &bar2) != 0); free_Bar(&bar1); free_Bar(&bar2); } TEST_CASE("model with null array is null") { const char *bar_json = BAR_WITH_NULL_CODES_ARRAY; Bar bar; REQUIRE(parse_Bar(&bar, bar_json, strlen(bar_json)) == strlen(bar_json)); CHECK(bar.codes == nullptr); free_Bar(&bar); } TEST_CASE("model with missing array is null") { const char *bar_json = BAR_WITH_MISSING_CODES_ARRAY; Bar bar; REQUIRE(parse_Bar(&bar, bar_json, strlen(bar_json)) == strlen(bar_json)); CHECK(bar.codes == nullptr); free_Bar(&bar); } #define string_map_model(XX, ...) \ XX(tags, model_string, map, tags, __VA_ARGS__) DECLARE_MODEL(tagged, string_map_model) IMPL_MODEL(tagged, string_map_model) TEST_CASE("model with string map", "[model]") { const char *json = R"({ "tags":{ "num":"42", "ok":"true", "msg":"hello\nworld!" } })"; tagged obj; REQUIRE(parse_tagged(&obj, json, strlen(json)) == strlen(json)); const char *num = (const char *) model_map_get(&obj.tags, "num"); CHECK_THAT(num, Equals("42")); CHECK_THAT((const char *) model_map_get(&obj.tags, "ok"), Equals("true")); CHECK_THAT((const char *) model_map_get(&obj.tags, "msg"), Equals("hello\nworld!")); char *buf = tagged_to_json(&obj, 0, nullptr); printf("%s", buf); free_tagged(&obj); free(buf); } #define objmap_model(XX, ...) \ XX(objects, Bar, map, objects, __VA_ARGS__) DECLARE_MODEL(MapOfObjects, objmap_model) IMPL_MODEL(MapOfObjects, objmap_model) TEST_CASE("map of objects", "[model]") { const char *json = "{" "\"objects\":{" "\"bar1\":" BAR1 "," "\"bar2\":" BAR2 "}}"; MapOfObjects m; REQUIRE(parse_MapOfObjects(&m, json, strlen(json)) == strlen(json)); Bar *b1 = static_cast(model_map_get(&m.objects, "bar1")); Bar *b2 = static_cast(model_map_get(&m.objects, "bar2")); REQUIRE(b1 != nullptr); REQUIRE(b2 != nullptr); CHECK(b1->num == 42); CHECK(!b1->isOK); CHECK_THAT(b1->msg, Equals("this is a message")); char *js = MapOfObjects_to_json(&m, MODEL_JSON_COMPACT, nullptr); std::cout << js << std::endl; free(js); char small_buf[16]; ssize_t outlen = MapOfObjects_to_json_r(&m, 0, small_buf, sizeof(small_buf)); CHECK(outlen == -1); char big_buf[1024]; outlen = MapOfObjects_to_json_r(&m, 0, big_buf, sizeof(big_buf)); CHECK(outlen > 0); std::cout << std::string(big_buf, outlen) << std::endl; free_MapOfObjects(&m); } #define basket_model(XX, ...) \ XX(json_fruits,json,map, json_fruits,__VA_ARGS__) \ XX(fruits,Fruit,map,fruits,__VA_ARGS__) \ XX(strings,model_string,map,strings,__VA_ARGS__) #define fruit_model(XX, ...) \ XX(color,model_string,none,color,__VA_ARGS__) \ XX(count,model_number,none,count,__VA_ARGS__) DECLARE_MODEL(Fruit, fruit_model) IMPL_MODEL(Fruit, fruit_model) DECLARE_MODEL(Basket, basket_model) IMPL_MODEL(Basket, basket_model) TEST_CASE("map compare", "[model]") { const char *json1 = R"({ "fruits" : { "orange" : { "color": "orange", "count": 1 }, "apple": { "color": "red", "count": 2 } }, "json_fruits" : { "orange" : { "color": "orange", "count": 1 }, "apple": { "color": "red", "count": 2 } }, "strings" : { "one": "1", "two": "2" } })"; const char *json2 = R"({ "json_fruits" : { "orange" : { "color": "orange", "count": 1 } }, "fruits" : { "orange" : { "color": "orange", "count": 1 } }, "strings" : { "two": "2" } })"; Basket b1, b2; parse_Basket(&b1, json1, strlen(json1)); parse_Basket(&b2, json2, strlen(json2)); int rc = cmp_Basket(&b1, &b2); CHECK(rc != 0); char *apple = (char *) model_map_remove(&b1.json_fruits, "apple"); free(apple); rc = cmp_Basket(&b1, &b2); CHECK(rc != 0); Fruit *app = (Fruit *) model_map_remove(&b1.fruits, "apple"); CHECK(app->count == 2); CHECK_THAT(app->color, Matches("red")); free_Fruit(app); free(app); char *one = (char *) model_map_remove(&b1.strings, "one"); CHECK_THAT(one, Matches("1")); free(one); rc = cmp_Basket(&b1, &b2); CHECK(rc == 0); free_Basket(&b1); free_Basket(&b2); } TEST_CASE("parse-json-u-escape", "[model]") { const char *json = "{" "\"bar\":{" "\"msg\":\"hello\\u000C\\u0430\\u0431\\u0432\\u0433\\u0434!\"" "}" "}"; Foo foo; REQUIRE(parse_Foo(&foo, json, strlen(json)) == strlen(json)); CHECK_THAT(foo.bar.msg, Catch::Matchers::Equals("hello\u000cабвгд!")); size_t json_len; char *json_out = Foo_to_json(&foo, 0, &json_len); CHECK(json_len == strlen(json_out)); CHECK_THAT(json_out, Catch::Matchers::ContainsSubstring("\"hello\\u000cабвгд!\"")); free(json_out); free_Foo(&foo); } TEST_CASE("parse-bad-json-escapes", "[model]") { const char *json[] = { // short u escape "{" "\"bar\":{" "\"msg\":\"hello\\u00C\\u0430\\u0431\\u0432\\u0433\\u0434!\"" "}" "}", // invalid char in u string "{" "\"bar\":{" "\"msg\":\"hello\\u00OC\\u0430\\u0431\\u0432\\u0433\\u0434!\"" "}" "}", // short escape at the end "{\"bar\":{" "\"msg\":\"hello\\u000C\\u0430\\u0431\\u0432\\u0433\\u\"" "}}", nullptr }; for (int i = 0; json[i] != nullptr; i++) { Foo foo; CHECK(parse_Foo(&foo, json[i], strlen(json[i])) < 0); } } TEST_CASE("null to JSON", "[model]") { char *json = Foo_to_json(nullptr, 0, nullptr); CHECK(json == nullptr); } #define LISTS_MODEL(xx, ...) \ xx(fruit, Fruit, list, fruit, __VA_ARGS__)\ xx(errors, model_string, list, errors, __VA_ARGS__)\ xx(codes, model_number, list, codes, __VA_ARGS__) DECLARE_MODEL(ListsObj, LISTS_MODEL) IMPL_MODEL(ListsObj, LISTS_MODEL) TEST_CASE("parse model_list", "[model]") { const char *json = R"([ { "color": "orange", "count": 1 }, { "color": "red", "count": 2 } ])"; model_list list = {0}; CHECK(parse_Fruit_list(&list, json, strlen(json)) == strlen(json)); CHECK(model_list_size(&list) == 2); auto it = model_list_iterator(&list); auto fruit = (Fruit *) model_list_it_element(it); CHECK(fruit->count == 1); CHECK_THAT(fruit->color, Catch::Matchers::Equals("orange")); it = model_list_it_next(it); fruit = (Fruit *) model_list_it_element(it); CHECK(fruit->count == 2); CHECK_THAT(fruit->color, Catch::Matchers::Equals("red")); CHECK(model_list_it_next(it) == nullptr); auto head = model_list_head(&list); fruit = (Fruit *) model_list_pop(&list); CHECK(head == fruit); CHECK(fruit != model_list_head(&list)); CHECK(model_list_size(&list) == 1); CHECK(fruit->count == 1); CHECK_THAT(fruit->color, Catch::Matchers::Equals("orange")); free_Fruit_ptr(fruit); model_list_clear(&list, (void (*)(void *)) (free_Fruit_ptr)); } TEST_CASE("lists model", "[model]") { const char *json = R"( { "errors": ["Not Authorized", "Conflict"], "codes": [401, 409, 413], "fruit": [ { "color": "orange", "count": 1 }, { "color": "red", "count": 2 } ] })"; ListsObj lists; CHECK(parse_ListsObj(&lists, json, strlen(json)) == strlen(json)); CHECK(model_list_size(&lists.codes) == 3); CHECK(model_list_size(&lists.errors) == 2); CHECK(model_list_size(&lists.fruit) == 2); auto it = model_list_iterator(&lists.errors); CHECK_THAT((const char *) model_list_it_element(it), Catch::Matchers::Equals("Not Authorized")); it = model_list_it_next(it); CHECK_THAT((const char *) model_list_it_element(it), Catch::Matchers::Equals("Conflict")); CHECK(model_list_it_next(it) == nullptr); it = model_list_iterator(&lists.codes); CHECK((intptr_t) model_list_it_element(it) == 401); it = model_list_it_next(it); CHECK((intptr_t) model_list_it_element(it) == 409); it = model_list_it_next(it); CHECK((intptr_t) model_list_it_element(it) == 413); CHECK(model_list_it_next(it) == nullptr); it = model_list_iterator(&lists.fruit); auto fruit = (Fruit *) model_list_it_element(it); CHECK(fruit->count == 1); CHECK_THAT(fruit->color, Catch::Matchers::Equals("orange")); it = model_list_it_next(it); fruit = (Fruit *) model_list_it_element(it); CHECK(fruit->count == 2); CHECK_THAT(fruit->color, Catch::Matchers::Equals("red")); CHECK(model_list_it_next(it) == nullptr); char *json_out = ListsObj_to_json(&lists, 0, nullptr); printf("%s\n", json_out); free_ListsObj(&lists); free(json_out); } #define DURATION_MODEL(XX, ...) \ XX(timeout, duration, none, timeout, __VA_ARGS__) DECLARE_MODEL(Duration, DURATION_MODEL) IMPL_MODEL(Duration, DURATION_MODEL) TEST_CASE("parse duration", "[model]") { using std::make_tuple; std::string test_input; duration expected_output = 3 * SECOND; std::tie( test_input, expected_output ) = GENERATE( table( { make_tuple(R"({"timeout": "3s"})", (duration)(3 * SECOND)), make_tuple(R"({"timeout": "3m"})", (duration)(3 * MINUTE)), make_tuple(R"({"timeout": "3h"})", 3 * HOUR), make_tuple(R"({"timeout": "3ms"})", 3 * MILLISECOND) }) ); Duration d = {0}; // run the test CHECK(parse_Duration(&d, test_input.c_str(), test_input.length()) == test_input.length()); // capture the input data to go with the outputs. CAPTURE(test_input); // check it matches the pre-calculated data REQUIRE(d.timeout == expected_output); }