diff --git a/include/cgimap/api06/changeset_upload/changeset_input_format.hpp b/include/cgimap/api06/changeset_upload/changeset_input_format.hpp index d82fc20d..81bd196a 100644 --- a/include/cgimap/api06/changeset_upload/changeset_input_format.hpp +++ b/include/cgimap/api06/changeset_upload/changeset_input_format.hpp @@ -60,7 +60,7 @@ namespace api06 { if (element == "osm") m_context = context::top; else - throw xml_error{ "Unknown top-level element, expecting osm" }; + throw payload_error{ "Unknown top-level element, expecting osm" }; break; @@ -70,7 +70,7 @@ namespace api06 { changeset_element_found = true; } else - throw xml_error{ "Unknown element, expecting changeset" }; + throw payload_error{ "Unknown element, expecting changeset" }; break; case context::in_changeset: @@ -79,7 +79,7 @@ namespace api06 { add_tag(attrs); } else - throw xml_error{ "Unknown element, expecting tag" }; + throw payload_error{ "Unknown element, expecting tag" }; break; case context::in_tag: @@ -100,7 +100,7 @@ namespace api06 { assert(element == "osm"); m_context = context::root; if (!changeset_element_found) - throw xml_error{ "Cannot parse valid changeset from xml string. XML doesn't contain an osm/changeset element" }; + throw payload_error{ "Cannot parse valid changeset from xml string. XML doesn't contain an osm/changeset element" }; break; case context::in_changeset: assert(element == "changeset"); @@ -116,7 +116,7 @@ namespace api06 { try { throw; - } catch (const xml_error& e) { + } catch (const payload_error& e) { throw_with_context(e, location); } } @@ -128,13 +128,13 @@ namespace api06 { void add_tag(const std::string &key, const std::string &value) { if (key.empty()) - throw xml_error("Key may not be empty"); + throw payload_error("Key may not be empty"); if (unicode_strlen(key) > 255) - throw xml_error("Key has more than 255 unicode characters"); + throw payload_error("Key has more than 255 unicode characters"); if (unicode_strlen(value) > 255) - throw xml_error("Value has more than 255 unicode characters"); + throw payload_error("Value has more than 255 unicode characters"); m_tags[key] = value; @@ -166,10 +166,10 @@ namespace api06 { }); if (!k) - throw xml_error{"Mandatory field k missing in tag element"}; + throw payload_error{"Mandatory field k missing in tag element"}; if (!v) - throw xml_error{"Mandatory field v missing in tag element"}; + throw payload_error{"Mandatory field v missing in tag element"}; add_tag(*k, *v); } diff --git a/include/cgimap/api06/changeset_upload/node.hpp b/include/cgimap/api06/changeset_upload/node.hpp index 1969c6fa..01978e61 100644 --- a/include/cgimap/api06/changeset_upload/node.hpp +++ b/include/cgimap/api06/changeset_upload/node.hpp @@ -37,11 +37,11 @@ class Node : public OSMObject { if (ec == std::errc()) set_lat(_lat); else if (ec == std::errc::invalid_argument) - throw xml_error("Latitude is not numeric"); + throw payload_error("Latitude is not numeric"); else if (ec == std::errc::result_out_of_range) - throw xml_error("Latitude value is too large"); + throw payload_error("Latitude value is too large"); else - throw xml_error("Unexpected parsing error"); + throw payload_error("Unexpected parsing error"); } void set_lon(const std::string &lon) { @@ -53,26 +53,26 @@ class Node : public OSMObject { if (ec == std::errc()) set_lon(_lon); else if (ec == std::errc::invalid_argument) - throw xml_error("Longitude is not numeric"); + throw payload_error("Longitude is not numeric"); else if (ec == std::errc::result_out_of_range) - throw xml_error("Longitude value is too large"); + throw payload_error("Longitude value is too large"); else - throw xml_error("Unexpected parsing error"); + throw payload_error("Unexpected parsing error"); } void set_lat(double lat) { if (lat < -90 || lat > 90) - throw xml_error("Latitude outside of valid range"); + throw payload_error("Latitude outside of valid range"); else if (!std::isfinite(lat)) - throw xml_error("Latitude not a valid finite number"); + throw payload_error("Latitude not a valid finite number"); m_lat = lat; } void set_lon(double lon) { if (lon < -180 || lon > 180) - throw xml_error("Longitude outside of valid range"); + throw payload_error("Longitude outside of valid range"); else if (!std::isfinite(lon)) - throw xml_error("Longitude not a valid finite number"); + throw payload_error("Longitude not a valid finite number"); m_lon = lon; } @@ -87,7 +87,13 @@ class Node : public OSMObject { } } - std::string get_type_name() override { return "Node"; } + std::string get_type_name() const override { return "Node"; } + + bool operator==(const Node &o) const { + return (OSMObject::operator==(o) && + o.m_lat == m_lat && + o.m_lon == m_lon); + } private: std::optional m_lat; diff --git a/include/cgimap/api06/changeset_upload/osmchange_json_input_format.hpp b/include/cgimap/api06/changeset_upload/osmchange_json_input_format.hpp new file mode 100644 index 00000000..70a9140e --- /dev/null +++ b/include/cgimap/api06/changeset_upload/osmchange_json_input_format.hpp @@ -0,0 +1,325 @@ +/** + * SPDX-License-Identifier: GPL-2.0-only + * + * This file is part of openstreetmap-cgimap (https://github.com/zerebubuth/openstreetmap-cgimap/). + * + * Copyright (C) 2009-2023 by the CGImap developer community. + * For a full list of authors see the git log. + */ + +#ifndef OSMCHANGE_JSON_INPUT_FORMAT_HPP +#define OSMCHANGE_JSON_INPUT_FORMAT_HPP + +#include "cgimap/api06/changeset_upload/node.hpp" +#include "cgimap/api06/changeset_upload/osmobject.hpp" +#include "cgimap/api06/changeset_upload/parser_callback.hpp" +#include "cgimap/api06/changeset_upload/relation.hpp" +#include "cgimap/api06/changeset_upload/way.hpp" +#include "cgimap/types.hpp" + +#include "sjparser/sjparser.h" + +#include + +#include +#include +#include +#include +#include + + +namespace api06 { + +using SJParser::Array; +using SJParser::Member; +using SJParser::Object; +using SJParser::Parser; +using SJParser::Presence; +using SJParser::SArray; +using SJParser::SAutoObject; +using SJParser::SMap; +using SJParser::Value; +using SJParser::Reaction; +using SJParser::ObjectOptions; + +using std::placeholders::_1; + +class OSMChangeJSONParserFormat { + + static auto getMemberParser() { + return SAutoObject{std::tuple{Member{"type", Value{}}, + Member{"ref", Value{}}, + Member{"role", Value{}, Presence::Optional, ""}}, + ObjectOptions{Reaction::Ignore} + }; + } + + template + static auto getElementsParser(ElementParserCallback element_parser_callback = nullptr) { + return Object{ + std::tuple{ + Member{"type", Value{}}, + Member{"action", Value{}}, + Member{"if-unused", Value{}, Presence::Optional, false}, + Member{"id", Value{}}, + Member{"lat", Value{}, Presence::Optional}, + Member{"lon", Value{}, Presence::Optional}, + Member{"version", Value{}, Presence::Optional}, + Member{"changeset", Value{}}, + Member{"tags", SMap{Value{}}, Presence::Optional}, + Member{"nodes", SArray{Value{}}, Presence::Optional}, + Member{"members", SArray{getMemberParser()}, Presence::Optional} + }, + ObjectOptions{Reaction::Ignore}, + element_parser_callback}; + } + + template + static auto getMainParser(ElementParserCallback element_parser_callback = nullptr) { + return Parser{ + Object{ + std::tuple{ + Member{"version", Value{}, Presence::Optional}, + Member{"generator", Value{}, Presence::Optional}, + Member{"osmChange", Array{getElementsParser(element_parser_callback)}} + },ObjectOptions{Reaction::Ignore}}}; + } + + friend class OSMChangeJSONParser; +}; + +class OSMChangeJSONParser { + +public: + explicit OSMChangeJSONParser(Parser_Callback& callback) + : m_callback(callback) { } + + OSMChangeJSONParser(const OSMChangeJSONParser &) = delete; + OSMChangeJSONParser &operator=(const OSMChangeJSONParser &) = delete; + + OSMChangeJSONParser(OSMChangeJSONParser &&) = delete; + OSMChangeJSONParser &operator=(OSMChangeJSONParser &&) = delete; + + void process_message(const std::string &data) { + + try { + m_callback.start_document(); + _parser.parse(data); + _parser.finish(); + + if (_parser.parser().isEmpty()) { + throw payload_error("Empty JSON payload"); + } + + if (element_count == 0) { + throw payload_error("osmChange array is empty"); + } + + m_callback.end_document(); + } catch (const std::exception& e) { + throw http::bad_request(e.what()); // rethrow JSON parser error as HTTP 400 Bad request + } + } + +private: + + using ElementsParser = decltype(api06::OSMChangeJSONParserFormat::getElementsParser()); + using MainParser = decltype(api06::OSMChangeJSONParserFormat::getMainParser()); + + MainParser _parser{api06::OSMChangeJSONParserFormat::getMainParser(std::bind(&api06::OSMChangeJSONParser::process_element, this, _1))}; + + // OSM element callback + bool process_element(ElementsParser &parser) { + + element_count++; + + // process action + process_action(parser); + + // process if-unused flag for delete action + process_if_unused(parser); + + // process type (node, way, relation) + process_type(parser); + + return true; + } + + void process_action(ElementsParser &parser) { + + const std::string& action = parser.get<1>(); + + if (action == "create") { + m_operation = operation::op_create; + } else if (action == "modify") { + m_operation = operation::op_modify; + } else if (action == "delete") { + m_operation = operation::op_delete; + } else { + throw payload_error{fmt::format("Unknown action {}, choices are create, modify, delete", action)}; + } + } + + void process_if_unused(ElementsParser &parser) { + + if (m_operation == operation::op_delete) { + m_if_unused = false; + if (parser.parser<2>().isSet()) { + m_if_unused = parser.get<2>(); + } + } + } + + void process_type(ElementsParser &parser) { + + const std::string& type = parser.get<0>(); + + if (type == "node") { + process_node(parser); + } else if (type == "way") { + process_way(parser); + } else if (type == "relation") { + process_relation(parser); + } else { + throw payload_error{fmt::format("Unknown element {}, expecting node, way or relation", type)}; + } + } + + void process_node(ElementsParser& parser) { + + Node node; + init_object(node, parser); + + if (parser.parser<4>().isSet()) { + node.set_lat(parser.get<4>()); + } + + if (parser.parser<5>().isSet()) { + node.set_lon(parser.get<5>()); + } + + process_tags(node, parser); + + if (!node.is_valid(m_operation)) { + throw payload_error{fmt::format("{} does not include all mandatory fields", node.to_string())}; + } + + m_callback.process_node(node, m_operation, m_if_unused); + } + + void process_way(ElementsParser& parser) { + + Way way; + init_object(way, parser); + + // adding way nodes + if (parser.parser<9>().isSet()) { + for (const auto& value : parser.get<9>()) { + way.add_way_node(value); + } + } + + process_tags(way, parser); + + if (!way.is_valid(m_operation)) { + throw payload_error{fmt::format("{} does not include all mandatory fields", way.to_string())}; + } + + m_callback.process_way(way, m_operation, m_if_unused); + } + + void process_relation(ElementsParser& parser) { + + Relation relation; + init_object(relation, parser); + + process_relation_members(relation, parser); + + process_tags(relation, parser); + + if (!relation.is_valid(m_operation)) { + throw payload_error{fmt::format("{} does not include all mandatory fields", relation.to_string())}; + } + + m_callback.process_relation(relation, m_operation, m_if_unused); + } + + void process_relation_members(Relation &relation, ElementsParser& parser) { + + if (!parser.parser<10>().isSet()) { + return; + } + + for (auto &mbr : parser.get<10>()) { + const auto& [type, ref, role] = mbr; + + RelationMember member; + member.set_type(type); + member.set_ref(ref); + member.set_role(role); + + if (!member.is_valid()) { + throw payload_error{fmt::format("Missing mandatory field on relation member in {}", relation.to_string()) }; + } + relation.add_member(member); + } + } + + void process_tags(OSMObject &o, ElementsParser& parser) { + + if (parser.parser<8>().isSet()) { + for (const auto &tag : parser.get<8>()) { + o.add_tag(tag.first, tag.second); + } + } + } + + void init_object(OSMObject &object, ElementsParser& parser) { + + // id + object.set_id(parser.get<3>()); + + // version + if (parser.parser<6>().isSet()) { + object.set_version(parser.get<6>()); + } + + // changeset + if (parser.parser<7>().isSet()) { + object.set_changeset(parser.get<7>()); + } + + // TODO: not needed, handled by sjparser + if (!object.has_id()) { + throw payload_error{ "Mandatory field id missing in object" }; + } + + if (!object.has_changeset()) { + throw payload_error{fmt::format("Changeset id is missing for {}", object.to_string()) }; + } + + if (m_operation == operation::op_create) { + // we always override version number for create operations (they are not + // mandatory) + object.set_version(0u); + } else if (m_operation == operation::op_delete || + m_operation == operation::op_modify) { + // objects for other operations must have a positive version number + if (!object.has_version()) { + throw payload_error{fmt::format("Version is required when updating {}", object.to_string()) }; + } + if (object.version() < 1) { + throw payload_error{ fmt::format("Invalid version number {} in {}", object.version(), object.to_string()) }; + } + } + } + + operation m_operation = operation::op_undefined; + Parser_Callback& m_callback; + bool m_if_unused = false; + int element_count = 0; +}; + +} // namespace api06 + +#endif // OSMCHANGE_JSON_INPUT_FORMAT_HPP diff --git a/include/cgimap/api06/changeset_upload/osmchange_input_format.hpp b/include/cgimap/api06/changeset_upload/osmchange_xml_input_format.hpp similarity index 92% rename from include/cgimap/api06/changeset_upload/osmchange_input_format.hpp rename to include/cgimap/api06/changeset_upload/osmchange_xml_input_format.hpp index fdb97bd1..f1b48bb3 100644 --- a/include/cgimap/api06/changeset_upload/osmchange_input_format.hpp +++ b/include/cgimap/api06/changeset_upload/osmchange_xml_input_format.hpp @@ -7,8 +7,8 @@ * For a full list of authors see the git log. */ -#ifndef OSMCHANGE_INPUT_FORMAT_HPP -#define OSMCHANGE_INPUT_FORMAT_HPP +#ifndef OSMCHANGE_XML_INPUT_FORMAT_HPP +#define OSMCHANGE_XML_INPUT_FORMAT_HPP #include "cgimap/api06/changeset_upload/node.hpp" #include "cgimap/api06/changeset_upload/osmobject.hpp" @@ -67,7 +67,7 @@ class OSMChangeXMLParser : private xmlpp::SaxParser { if (element == "osmChange") { m_callback.start_document(); } else { - throw xml_error{ + throw payload_error{ fmt::format("Unknown top-level element {}, expecting osmChange", element) }; @@ -94,7 +94,7 @@ class OSMChangeXMLParser : private xmlpp::SaxParser { m_context.push_back(context::in_delete); m_operation = operation::op_delete; } else { - throw xml_error{ + throw payload_error{ fmt::format( "Unknown action {}, choices are create, modify, delete", element) @@ -120,7 +120,7 @@ class OSMChangeXMLParser : private xmlpp::SaxParser { init_object(*m_relation, attrs); m_context.push_back(context::relation); } else { - throw xml_error{ + throw payload_error{ fmt::format( "Unknown element {}, expecting node, way or relation", element) @@ -145,7 +145,7 @@ class OSMChangeXMLParser : private xmlpp::SaxParser { } }); if (!ref_found) - throw xml_error{fmt::format( + throw payload_error{fmt::format( "Missing mandatory ref field on way node {}", m_way->to_string()) }; } else if (element == "tag") { @@ -166,7 +166,7 @@ class OSMChangeXMLParser : private xmlpp::SaxParser { } }); if (!member.is_valid()) { - throw xml_error{ fmt::format( + throw payload_error{ fmt::format( "Missing mandatory field on relation member in {}", m_relation->to_string()) }; } @@ -176,7 +176,7 @@ class OSMChangeXMLParser : private xmlpp::SaxParser { } break; case context::in_object: - throw xml_error{ "xml file nested too deep" }; + throw payload_error{ "xml file nested too deep" }; break; } } @@ -216,7 +216,7 @@ class OSMChangeXMLParser : private xmlpp::SaxParser { case context::node: assert(element == "node"); if (!m_node->is_valid(m_operation)) { - throw xml_error{ + throw payload_error{ fmt::format("{} does not include all mandatory fields", m_node->to_string()) }; @@ -228,7 +228,7 @@ class OSMChangeXMLParser : private xmlpp::SaxParser { case context::way: assert(element == "way"); if (!m_way->is_valid(m_operation)) { - throw xml_error{ + throw payload_error{ fmt::format("{} does not include all mandatory fields", m_way->to_string()) }; @@ -241,7 +241,7 @@ class OSMChangeXMLParser : private xmlpp::SaxParser { case context::relation: assert(element == "relation"); if (!m_relation->is_valid(m_operation)) { - throw xml_error{ + throw payload_error{ fmt::format("{} does not include all mandatory fields", m_relation->to_string()) }; @@ -260,7 +260,7 @@ class OSMChangeXMLParser : private xmlpp::SaxParser { try { throw; - } catch (const xml_error& e) { + } catch (const payload_error& e) { throw_with_context(e, location); } } @@ -304,11 +304,11 @@ class OSMChangeXMLParser : private xmlpp::SaxParser { }); if (!object.has_id()) { - throw xml_error{ "Mandatory field id missing in object" }; + throw payload_error{ "Mandatory field id missing in object" }; } if (!object.has_changeset()) { - throw xml_error{ fmt::format("Changeset id is missing for {}", + throw payload_error{ fmt::format("Changeset id is missing for {}", object.to_string()) }; } @@ -320,12 +320,12 @@ class OSMChangeXMLParser : private xmlpp::SaxParser { m_operation == operation::op_modify) { // objects for other operations must have a positive version number if (!object.has_version()) { - throw xml_error{ fmt::format( + throw payload_error{ fmt::format( "Version is required when updating {}", object.to_string()) }; } if (object.version() < 1) { - throw xml_error{ fmt::format("Invalid version number {} in {}", + throw payload_error{ fmt::format("Invalid version number {} in {}", object.version(), object.to_string()) }; } } @@ -357,13 +357,13 @@ class OSMChangeXMLParser : private xmlpp::SaxParser { }); if (!k) - throw xml_error{ + throw payload_error{ fmt::format("Mandatory field k missing in tag element for {}", o.to_string()) }; if (!v) - throw xml_error{ + throw payload_error{ fmt::format("Mandatory field v missing in tag element for {}", o.to_string()) }; @@ -400,4 +400,4 @@ class OSMChangeXMLParser : private xmlpp::SaxParser { } // namespace api06 -#endif // OSMCHANGE_INPUT_FORMAT_HPP +#endif // OSMCHANGE_INPUT_XML_FORMAT_HPP diff --git a/include/cgimap/api06/changeset_upload/osmobject.hpp b/include/cgimap/api06/changeset_upload/osmobject.hpp index 79fabd5e..d9cec4a8 100644 --- a/include/cgimap/api06/changeset_upload/osmobject.hpp +++ b/include/cgimap/api06/changeset_upload/osmobject.hpp @@ -21,12 +21,12 @@ namespace api06 { - struct xml_error : public http::bad_request { + struct payload_error : public http::bad_request { std::string error_code; std::string error_string; - explicit xml_error(const std::string &message) + explicit payload_error(const std::string &message) : http::bad_request(message), error_string(message) {} }; @@ -40,7 +40,7 @@ namespace api06 { void set_changeset(osm_changeset_id_t changeset) { if (changeset <= 0) { - throw xml_error("Changeset must be a positive number"); + throw payload_error("Changeset must be a positive number"); } m_changeset = changeset; @@ -49,7 +49,7 @@ namespace api06 { void set_version(osm_version_t version) { if (version < 0) { - throw xml_error("Version may not be negative"); + throw payload_error("Version may not be negative"); } m_version = version; @@ -58,7 +58,7 @@ namespace api06 { void set_id(osm_nwr_signed_id_t id) { if (id == 0) { - throw xml_error("Id must be different from 0"); + throw payload_error("Id must be different from 0"); } m_id = id; @@ -75,11 +75,11 @@ namespace api06 { if (ec == std::errc()) set_changeset(_changeset); else if (ec == std::errc::invalid_argument) - throw xml_error("Changeset is not numeric"); + throw payload_error("Changeset is not numeric"); else if (ec == std::errc::result_out_of_range) - throw xml_error("Changeset number is too large"); + throw payload_error("Changeset number is too large"); else - throw xml_error("Unexpected parsing error"); + throw payload_error("Unexpected parsing error"); } void set_version(const std::string &version) { @@ -91,11 +91,11 @@ namespace api06 { if (ec == std::errc()) set_version(_version); else if (ec == std::errc::invalid_argument) - throw xml_error("Version is not numeric"); + throw payload_error("Version is not numeric"); else if (ec == std::errc::result_out_of_range) - throw xml_error("Version value is too large"); + throw payload_error("Version value is too large"); else - throw xml_error("Unexpected parsing error"); + throw payload_error("Unexpected parsing error"); } void set_id(const std::string &id) { @@ -107,11 +107,11 @@ namespace api06 { if (ec == std::errc()) set_id(_id); else if (ec == std::errc::invalid_argument) - throw xml_error("Id is not numeric"); + throw payload_error("Id is not numeric"); else if (ec == std::errc::result_out_of_range) - throw xml_error("Id number is too large"); + throw payload_error("Id number is too large"); else - throw xml_error("Unexpected parsing error"); + throw payload_error("Unexpected parsing error"); } osm_changeset_id_t changeset() const { return *m_changeset; } @@ -124,28 +124,33 @@ namespace api06 { constexpr bool has_id() const { return m_id.has_value(); }; constexpr bool has_version() const { return m_version.has_value(); } - std::map tags() const { return m_tags; } + void add_tags(const std::map& tags) { + for (const auto& [key, value] : tags) { + add_tag(key, value); + } + } + void add_tag(const std::string& key, const std::string& value) { if (key.empty()) { - throw xml_error(fmt::format("Key may not be empty in {}", to_string())); + throw payload_error(fmt::format("Key may not be empty in {}", to_string())); } if (unicode_strlen(key) > 255) { - throw xml_error( + throw payload_error( fmt::format("Key has more than 255 unicode characters in {}", to_string())); } if (unicode_strlen(value) > 255) { - throw xml_error( + throw payload_error( fmt::format("Value has more than 255 unicode characters in {}", to_string())); } if (!(m_tags.insert({key, value})) .second) { - throw xml_error( + throw payload_error( fmt::format("{} has duplicate tags with key {}", to_string(), key)); } } @@ -153,12 +158,12 @@ namespace api06 { virtual bool is_valid() const { // check if all mandatory fields have been set if (!m_changeset) - throw xml_error( + throw payload_error( "You need to supply a changeset to be able to make a change"); if ((global_settings::get_element_max_tags()) && m_tags.size() > *global_settings::get_element_max_tags()) { - throw xml_error( + throw payload_error( fmt::format("OSM element exceeds limit of {} tags", *global_settings::get_element_max_tags())); } @@ -166,13 +171,20 @@ namespace api06 { return (m_changeset && m_id && m_version); } - virtual std::string get_type_name() = 0; + virtual std::string get_type_name() const = 0; - virtual std::string to_string() { + virtual std::string to_string() const { return fmt::format("{} {:d}", get_type_name(), m_id.value_or(0)); } + bool operator==(const OSMObject &o) const { + return (o.m_changeset == m_changeset && + o.m_id == m_id && + o.m_version == m_version && + o.m_tags == m_tags); + } + private: std::optional m_changeset; std::optional m_id; diff --git a/include/cgimap/api06/changeset_upload/relation.hpp b/include/cgimap/api06/changeset_upload/relation.hpp index cef88910..cfc06eb5 100644 --- a/include/cgimap/api06/changeset_upload/relation.hpp +++ b/include/cgimap/api06/changeset_upload/relation.hpp @@ -26,8 +26,8 @@ class RelationMember { RelationMember() = default; RelationMember(const std::string &m_type, osm_nwr_signed_id_t m_ref, const std::string &m_role) : - m_role(m_role), - m_ref(m_ref), + m_role(m_role), + m_ref(m_ref), m_type(m_type) {} void set_type(const std::string &type) { @@ -39,14 +39,14 @@ class RelationMember { else if (boost::iequals(type, "Relation")) m_type = "Relation"; else - throw xml_error( + throw payload_error( fmt::format("Invalid type {} in member relation", type)); } void set_role(const std::string &role) { if (unicode_strlen(role) > 255) { - throw xml_error( + throw payload_error( "Relation Role has more than 255 unicode characters"); } @@ -56,7 +56,7 @@ class RelationMember { void set_ref(osm_nwr_signed_id_t ref) { if (ref == 0) { - throw xml_error("Relation member 'ref' attribute may not be 0"); + throw payload_error("Relation member 'ref' attribute may not be 0"); } m_ref = ref; @@ -72,20 +72,20 @@ class RelationMember { set_ref(_ref); } else if (ec == std::errc::invalid_argument) - throw xml_error("Relation member 'ref' attribute is not numeric"); + throw payload_error("Relation member 'ref' attribute is not numeric"); else if (ec == std::errc::result_out_of_range) - throw xml_error("Relation member 'ref' attribute value is too large"); + throw payload_error("Relation member 'ref' attribute value is too large"); else - throw xml_error("Unexpected parsing error"); + throw payload_error("Unexpected parsing error"); } bool is_valid() const { if (!m_type) - throw xml_error("Missing 'type' attribute in Relation member"); + throw payload_error("Missing 'type' attribute in Relation member"); if (!m_ref) - throw xml_error("Missing 'ref' attribute in Relation member"); + throw payload_error("Missing 'ref' attribute in Relation member"); return (m_ref && m_type); } @@ -96,6 +96,12 @@ class RelationMember { osm_nwr_signed_id_t ref() const { return *m_ref; } + bool operator==(const RelationMember &o) const { + return (o.m_role == m_role && + o.m_ref == m_ref && + o.m_type == m_type); + } + private: std::string m_role; std::optional m_ref; @@ -108,9 +114,14 @@ class Relation : public OSMObject { ~Relation() override = default; + void add_members(std::vector&& members) { + for (auto& mbr : members) + add_member(mbr); + } + void add_member(RelationMember &member) { if (!member.is_valid()) - throw xml_error( + throw payload_error( "Relation member does not include all mandatory fields"); m_relation_member.emplace_back(member); } @@ -119,7 +130,7 @@ class Relation : public OSMObject { return m_relation_member; } - std::string get_type_name() override { return "Relation"; } + std::string get_type_name() const override { return "Relation"; } bool is_valid(operation op) const { @@ -142,6 +153,11 @@ class Relation : public OSMObject { } } + bool operator==(const Relation &o) const { + return (OSMObject::operator==(o) && + o.m_relation_member == m_relation_member); + } + private: std::vector m_relation_member; using OSMObject::is_valid; diff --git a/include/cgimap/api06/changeset_upload/way.hpp b/include/cgimap/api06/changeset_upload/way.hpp index 9a524abd..a33960a9 100644 --- a/include/cgimap/api06/changeset_upload/way.hpp +++ b/include/cgimap/api06/changeset_upload/way.hpp @@ -27,9 +27,14 @@ class Way : public OSMObject { ~Way() override = default; + void add_way_nodes(const std::vector& way_nodes) { + for (auto const wn : way_nodes) + m_way_nodes.emplace_back(wn); + } + void add_way_node(osm_nwr_signed_id_t waynode) { if (waynode == 0) { - throw xml_error("Way node value may not be 0"); + throw payload_error("Way node value may not be 0"); } m_way_nodes.emplace_back(waynode); @@ -44,11 +49,11 @@ class Way : public OSMObject { if (ec == std::errc()) add_way_node(_waynode); else if (ec == std::errc::invalid_argument) - throw xml_error("Way node is not numeric"); + throw payload_error("Way node is not numeric"); else if (ec == std::errc::result_out_of_range) - throw xml_error("Way node value is too large"); + throw payload_error("Way node value is too large"); else - throw xml_error("Unexpected parsing error"); + throw payload_error("Unexpected parsing error"); } const std::vector &nodes() const { return m_way_nodes; } @@ -78,7 +83,12 @@ class Way : public OSMObject { } } - std::string get_type_name() override { return "Way"; } + std::string get_type_name() const override { return "Way"; } + + bool operator==(const Way &o) const { + return (OSMObject::operator==(o) && + o.m_way_nodes == m_way_nodes); + } private: std::vector m_way_nodes; diff --git a/include/cgimap/json_formatter.hpp b/include/cgimap/json_formatter.hpp index fccb2097..7ed80be1 100644 --- a/include/cgimap/json_formatter.hpp +++ b/include/cgimap/json_formatter.hpp @@ -23,6 +23,7 @@ class json_formatter : public output_formatter { private: std::unique_ptr writer; bool is_in_elements_array{false}; + bool is_in_diffresult_array{false}; void write_tags(const tags_t &tags); void write_id(const element_info &elem); @@ -38,6 +39,7 @@ class json_formatter : public output_formatter { void end_document() override; void write_bounds(const bbox &bounds) override; void start_element_type(element_type type) override; + void start_diffresult() override; void end_element_type(element_type type) override; void start_action(action_type type) override; void end_action(action_type type) override; @@ -60,6 +62,7 @@ class json_formatter : public output_formatter { const osm_nwr_signed_id_t old_id, const osm_nwr_id_t new_id, const osm_version_t new_version) override; + void write_diffresult_delete(const element_type elem, const osm_nwr_signed_id_t old_id) override; diff --git a/include/cgimap/osm_diffresult_responder.hpp b/include/cgimap/osm_diffresult_responder.hpp index 2d5498c2..e6b421f4 100644 --- a/include/cgimap/osm_diffresult_responder.hpp +++ b/include/cgimap/osm_diffresult_responder.hpp @@ -27,6 +27,8 @@ class osm_diffresult_responder : public osm_responder { ~osm_diffresult_responder() override; + // lists the standard types that OSM format can respond in + std::vector types_available() const override; void write(output_formatter& f, const std::string &generator, diff --git a/include/cgimap/output_formatter.hpp b/include/cgimap/output_formatter.hpp index e675a52f..cd37b29f 100644 --- a/include/cgimap/output_formatter.hpp +++ b/include/cgimap/output_formatter.hpp @@ -56,6 +56,23 @@ T element_type_name(element_type elt) noexcept { return ""; } +template +T action_type_name(action_type action) noexcept { + + switch (action) { + case action_type::create: + return "create"; + break; + case action_type::modify: + return "modify"; + break; + case action_type::del: + return "delete"; + break; + } + return ""; +} + } // anonymous namespace struct element_info { @@ -214,6 +231,9 @@ struct output_formatter { // end a type of element. this is called once for nodes, ways or relations virtual void end_element_type(element_type type) = 0; + // marks the beginning of diffResult response processing + virtual void start_diffresult() = 0; + // TODO: document me. virtual void start_action(action_type type) = 0; virtual void end_action(action_type type) = 0; diff --git a/include/cgimap/text_formatter.hpp b/include/cgimap/text_formatter.hpp index 67fc5fbe..1ac4074b 100644 --- a/include/cgimap/text_formatter.hpp +++ b/include/cgimap/text_formatter.hpp @@ -34,6 +34,7 @@ class text_formatter : public output_formatter { void write_bounds(const bbox &bounds) override; void start_element_type(element_type type) override; void end_element_type(element_type type) override; + void start_diffresult() override; void start_action(action_type type) override; void end_action(action_type type) override; void error(const std::exception &e) override; diff --git a/include/cgimap/xml_formatter.hpp b/include/cgimap/xml_formatter.hpp index 7fe654e6..67cbd770 100644 --- a/include/cgimap/xml_formatter.hpp +++ b/include/cgimap/xml_formatter.hpp @@ -34,6 +34,7 @@ class xml_formatter : public output_formatter { void end_document() override; void write_bounds(const bbox &bounds) override; void start_element_type(element_type type) override; + void start_diffresult() override; void end_element_type(element_type type) override; void start_action(action_type type) override; void end_action(action_type type) override; diff --git a/src/api06/changeset_upload_handler.cpp b/src/api06/changeset_upload_handler.cpp index 5aabf9a9..9141cf93 100644 --- a/src/api06/changeset_upload_handler.cpp +++ b/src/api06/changeset_upload_handler.cpp @@ -13,7 +13,8 @@ #include "cgimap/request_context.hpp" #include "cgimap/api06/changeset_upload/osmchange_handler.hpp" -#include "cgimap/api06/changeset_upload/osmchange_input_format.hpp" +#include "cgimap/api06/changeset_upload/osmchange_xml_input_format.hpp" +#include "cgimap/api06/changeset_upload/osmchange_json_input_format.hpp" #include "cgimap/api06/changeset_upload/osmchange_tracking.hpp" #include "cgimap/api06/changeset_upload_handler.hpp" #include "cgimap/backend/apidb/changeset_upload/changeset_updater.hpp" @@ -31,9 +32,9 @@ namespace api06 { -changeset_upload_responder::changeset_upload_responder(mime::type mt, - data_update& upd, - osm_changeset_id_t changeset, +changeset_upload_responder::changeset_upload_responder(mime::type mt, + data_update& upd, + osm_changeset_id_t changeset, const std::string &payload, const RequestContext& req_ctx) : osm_diffresult_responder(mt) { @@ -54,9 +55,13 @@ changeset_upload_responder::changeset_upload_responder(mime::type mt, OSMChange_Handler handler(*node_updater, *way_updater, *relation_updater, changeset); - OSMChangeXMLParser parser(handler); - - parser.process_message(payload); + // TODO: check HTTP Accept header + if (mt != mime::type::application_json) { + OSMChangeXMLParser(handler).process_message(payload); + } + else { + OSMChangeJSONParser(handler).process_message(payload); + } // store diffresult for output handling in class osm_diffresult_responder m_diffresult = change_tracking.assemble_diffresult(); diff --git a/src/json_formatter.cpp b/src/json_formatter.cpp index 74c478b1..a9e0adca 100644 --- a/src/json_formatter.cpp +++ b/src/json_formatter.cpp @@ -56,6 +56,7 @@ void json_formatter::end_document() { writer->end_array(); // end of elements array is_in_elements_array = false; + is_in_diffresult_array = false; writer->end_object(); } @@ -69,11 +70,19 @@ void json_formatter::start_element_type(element_type type) { is_in_elements_array = true; } -void json_formatter::end_element_type(element_type type) {} +void json_formatter::start_diffresult() { + if (is_in_diffresult_array) + return; -void json_formatter::start_action(action_type type) { + writer->object_key("diffResult"); + writer->start_array(); + is_in_diffresult_array = true; } +void json_formatter::end_element_type(element_type type) {} + +void json_formatter::start_action(action_type type) {} + void json_formatter::end_action(action_type type) { } @@ -229,28 +238,22 @@ void json_formatter::write_diffresult_create_modify(const element_type elem, const osm_version_t new_version) { -// writer->start_object(); -// writer->object_key("type"); -// writer->entry_string(element_type_name(elem)); -// writer->object_key("old_id"); -// writer->entry_int(old_id); -// writer->object_key("new_id"); -// writer->entry_int(new_id); -// writer->object_key("new_version"); -// writer->entry_int(new_version); -// writer->end_object(); + writer->start_object(); + writer->property("type", element_type_name(elem)); + writer->property("old_id", old_id); + writer->property("new_id", new_id); + writer->property("new_version", new_version); + writer->end_object(); } void json_formatter::write_diffresult_delete(const element_type elem, const osm_nwr_signed_id_t old_id) { -// writer->start_object(); -// writer->object_key("type"); -// writer->entry_string(element_type_name(elem)); -// writer->object_key("old_id"); -// writer->entry_int(old_id); -// writer->end_object(); + writer->start_object(); + writer->property("type", element_type_name(elem)); + writer->property("old_id", old_id); + writer->end_object(); } void json_formatter::flush() { writer->flush(); } diff --git a/src/osm_diffresult_responder.cpp b/src/osm_diffresult_responder.cpp index bb031552..326eaa9f 100644 --- a/src/osm_diffresult_responder.cpp +++ b/src/osm_diffresult_responder.cpp @@ -32,7 +32,6 @@ namespace { throw std::runtime_error("Unhandled object_type in as_elem_type."); } - } osm_diffresult_responder::osm_diffresult_responder(mime::type mt) @@ -40,6 +39,13 @@ osm_diffresult_responder::osm_diffresult_responder(mime::type mt) osm_diffresult_responder::~osm_diffresult_responder() = default; +std::vector osm_diffresult_responder::types_available() const { + std::vector types; + types.push_back(mime::type::application_xml); + types.push_back(mime::type::application_json); + return types; +} + void osm_diffresult_responder::write(output_formatter& fmt, const std::string &generator, const std::chrono::system_clock::time_point &) { @@ -48,6 +54,8 @@ void osm_diffresult_responder::write(output_formatter& fmt, try { fmt.start_document(generator, "diffResult"); + fmt.start_diffresult(); + // Iterate over all elements in the sequence defined in the osmChange // message for (const auto &item : m_diffresult) { diff --git a/src/osmchange_responder.cpp b/src/osmchange_responder.cpp index 506409d5..a8576339 100644 --- a/src/osmchange_responder.cpp +++ b/src/osmchange_responder.cpp @@ -177,6 +177,11 @@ struct sorting_formatter : public output_formatter { throw std::runtime_error("Unexpected call to end_action."); } + void start_diffresult() override { + // this shouldn't be called here + throw std::runtime_error("Unexpected call to start_diffresult."); + } + void write(output_formatter &fmt) { std::sort(m_elements.begin(), m_elements.end()); for (const auto &e : m_elements) { diff --git a/src/text_formatter.cpp b/src/text_formatter.cpp index 7fa071c8..ce79886b 100644 --- a/src/text_formatter.cpp +++ b/src/text_formatter.cpp @@ -39,6 +39,10 @@ void text_formatter::end_element_type(element_type) { // nothing needed here } +void text_formatter::start_diffresult() { + // nothing needed here +} + void text_formatter::start_action(action_type type) { // nothing needed here } diff --git a/src/xml_formatter.cpp b/src/xml_formatter.cpp index 14a5e827..6135eaac 100644 --- a/src/xml_formatter.cpp +++ b/src/xml_formatter.cpp @@ -52,18 +52,12 @@ void xml_formatter::end_element_type(element_type) { // ditto - nothing needed here for XML. } +void xml_formatter::start_diffresult() { + // not needed in case of xml +} + void xml_formatter::start_action(action_type type) { - switch (type) { - case action_type::create: - writer->start("create"); - break; - case action_type::modify: - writer->start("modify"); - break; - case action_type::del: - writer->start("delete"); - break; - } + writer->start(action_type_name(type)); } void xml_formatter::end_action(action_type type) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b4ef5ffc..4046d93e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -109,21 +109,35 @@ if(BUILD_TESTING) COMMAND test_parse_time) - ############################ - # test_parse_osmchange_input - ############################ - add_executable(test_parse_osmchange_input - test_parse_osmchange_input.cpp) + ################################ + # test_parse_osmchange_xml_input + ################################ + add_executable(test_parse_osmchange_xml_input + test_parse_osmchange_xml_input.cpp) - target_link_libraries(test_parse_osmchange_input + target_link_libraries(test_parse_osmchange_xml_input cgimap_common_compiler_options cgimap_core Boost::program_options catch2) - add_test(NAME test_parse_osmchange_input - COMMAND test_parse_osmchange_input) + add_test(NAME test_parse_osmchange_xml_input + COMMAND test_parse_osmchange_xml_input) + + ################################# + # test_parse_osmchange_json_input + ################################# + add_executable(test_parse_osmchange_json_input + test_parse_osmchange_json_input.cpp) + + target_link_libraries(test_parse_osmchange_json_input + cgimap_common_compiler_options + cgimap_core + Boost::program_options + catch2) + add_test(NAME test_parse_osmchange_json_input + COMMAND test_parse_osmchange_json_input) ############################ # test_parse_changeset_input @@ -273,7 +287,8 @@ if(BUILD_TESTING) test_oauth2 test_http test_parse_time - test_parse_osmchange_input + test_parse_osmchange_xml_input + test_parse_osmchange_json_input test_parse_changeset_input) add_dependencies(check test_apidb_backend_nodes diff --git a/test/test_apidb_backend_changeset_uploads.cpp b/test/test_apidb_backend_changeset_uploads.cpp index 70180c15..8c82b364 100644 --- a/test/test_apidb_backend_changeset_uploads.cpp +++ b/test/test_apidb_backend_changeset_uploads.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,10 @@ #include #include #include +#include +#include +#include +#include #include @@ -34,7 +39,7 @@ #include "cgimap/request_context.hpp" #include "cgimap/api06/changeset_upload/osmchange_handler.hpp" -#include "cgimap/api06/changeset_upload/osmchange_input_format.hpp" +#include "cgimap/api06/changeset_upload/osmchange_xml_input_format.hpp" #include "test_formatter.hpp" #include "test_database.hpp" @@ -47,6 +52,9 @@ using Catch::Matchers::StartsWith; using Catch::Matchers::EndsWith; using Catch::Matchers::Equals; +namespace al = boost::algorithm; +namespace pt = boost::property_tree; + class global_settings_enable_upload_rate_limiter_test_class : public global_settings_default { public: @@ -1926,7 +1934,7 @@ TEST_CASE_METHOD( DatabaseTestsFixture, "test_osmchange_message", "[changeset][u - + )"), http::bad_request, Catch::Message("Placeholder relation not found for reference -4 in relation -3")); } @@ -1952,7 +1960,7 @@ TEST_CASE_METHOD( DatabaseTestsFixture, "test_osmchange_message", "[changeset][u - + @@ -1963,7 +1971,7 @@ TEST_CASE_METHOD( DatabaseTestsFixture, "test_osmchange_message", "[changeset][u - + )")); REQUIRE(diffresult.size() == 5); @@ -2014,7 +2022,7 @@ TEST_CASE_METHOD( DatabaseTestsFixture, "test_osmchange_end_to_end", "[changeset VALUES (1, 'demo@example.com', 'x', '', '2013-11-14T02:10:00Z', 'demo', true, 'confirmed'), (2, 'user_2@example.com', 'x', '', '2013-11-14T02:10:00Z', 'user_2', false, 'active'); - + INSERT INTO changesets (id, user_id, created_at, closed_at, num_changes) VALUES (1, 1, now() at time zone 'utc', now() at time zone 'utc' + '1 hour' ::interval, 0), @@ -2023,15 +2031,15 @@ TEST_CASE_METHOD( DatabaseTestsFixture, "test_osmchange_end_to_end", "[changeset now() at time zone 'utc' - '11 hour' ::interval, 10000), (4, 2, now() at time zone 'utc', now() at time zone 'utc' + '1 hour' ::interval, 0), (5, 2, '2013-11-14T02:10:00Z', '2013-11-14T03:10:00Z', 0); - + INSERT INTO user_blocks (user_id, creator_id, reason, ends_at, needs_view) VALUES (1, 2, '', now() at time zone 'utc' - ('1 hour' ::interval), false); - INSERT INTO oauth_applications (id, owner_type, owner_id, name, uid, secret, redirect_uri, scopes, confidential, created_at, updated_at) - VALUES (3, 'User', 1, 'App 1', 'dHKmvGkmuoMjqhCNmTJkf-EcnA61Up34O1vOHwTSvU8', '965136b8fb8d00e2faa2faaaed99c0ec10225518d0c8d9fb1d2af701e87eb68c', + INSERT INTO oauth_applications (id, owner_type, owner_id, name, uid, secret, redirect_uri, scopes, confidential, created_at, updated_at) + VALUES (3, 'User', 1, 'App 1', 'dHKmvGkmuoMjqhCNmTJkf-EcnA61Up34O1vOHwTSvU8', '965136b8fb8d00e2faa2faaaed99c0ec10225518d0c8d9fb1d2af701e87eb68c', 'http://demo.localhost:3000', 'write_api read_gpx', false, '2021-04-12 17:53:30', '2021-04-12 17:53:30'); - - INSERT INTO public.oauth_access_tokens (id, resource_owner_id, application_id, token, refresh_token, expires_in, revoked_at, created_at, scopes, previous_refresh_token) + + INSERT INTO public.oauth_access_tokens (id, resource_owner_id, application_id, token, refresh_token, expires_in, revoked_at, created_at, scopes, previous_refresh_token) VALUES (67, 1, 3, '4f41f2328befed5a33bcabdf14483081c8df996cbafc41e313417776e8fafae8', NULL, NULL, NULL, '2021-04-14 19:38:21', 'write_api', ''); )" ); @@ -2446,6 +2454,66 @@ TEST_CASE_METHOD( DatabaseTestsFixture, "test_osmchange_end_to_end", "[changeset REQUIRE(req.response_status() == 200); } + SECTION("JSON upload") + { + std::string payload = R"( + { + "version": "0.6", + "generator": "demo", + "osmChange": [ + { + "type": "node", + "action": "create", + "id": -1, + "lat": 42, + "lon": 13, + "changeset": 1 + }, + { + "type": "node", + "action": "modify", + "id": -1, + "version": 1, + "lat": 42.7957187, + "lon": 13.5690032, + "changeset": 1, + "tags": { + "man_made": "mast", + "name": "Monte Piselli - San Giacomo" + } + } + ] + } + )"; + + req.set_header("REQUEST_URI", "/api/0.6/changeset/1/upload.json"); + req.set_payload(payload); + + // execute the request + process_request(req, limiter, generator, route, *sel_factory, upd_factory.get()); + + CAPTURE(req.body().str()); + + REQUIRE(req.response_status() == 200); + + SECTION("Validate diffResult in JSON format") + { + pt::ptree act_tree; + std::stringstream ss(req.body().str()); + pt::read_json(ss, act_tree); + + auto diffResult = act_tree.get_child("diffResult"); + int version = 1; + for (auto & entry : diffResult) { + REQUIRE(entry.second.get("type") == "node"); + REQUIRE(entry.second.get("old_id") == -1); + REQUIRE(entry.second.get("new_id") > 0); + REQUIRE(entry.second.get("new_version") == version); + version++; + } + } + } + } @@ -2471,7 +2539,7 @@ TEST_CASE_METHOD( DatabaseTestsFixture, "test_osmchange_rate_limiter", "[changes VALUES (1, 'demo@example.com', 'xx', '', '2013-11-14T02:10:00Z', 'demo', true, 'confirmed'), (2, 'user_2@example.com', '', '', '2013-11-14T02:10:00Z', 'user_2', false, 'active'); - + INSERT INTO changesets (id, user_id, created_at, closed_at, num_changes) VALUES (1, 1, now() at time zone 'utc', now() at time zone 'utc' + '1 hour' ::interval, 0), @@ -2479,19 +2547,19 @@ TEST_CASE_METHOD( DatabaseTestsFixture, "test_osmchange_rate_limiter", "[changes now() at time zone 'utc' - '11 hour' ::interval, 10000), (4, 2, now() at time zone 'utc', now() at time zone 'utc' + '1 hour' ::interval, 10000), (5, 2, '2013-11-14T02:10:00Z', '2013-11-14T03:10:00Z', 10000); - + INSERT INTO user_blocks (user_id, creator_id, reason, ends_at, needs_view) VALUES (1, 2, '', now() at time zone 'utc' - ('1 hour' ::interval), false); SELECT setval('current_nodes_id_seq', 14000000000, false); - INSERT INTO oauth_applications (id, owner_type, owner_id, name, uid, secret, redirect_uri, scopes, confidential, created_at, updated_at) - VALUES (3, 'User', 1, 'App 1', 'dHKmvGkmuoMjqhCNmTJkf-EcnA61Up34O1vOHwTSvU8', '965136b8fb8d00e2faa2faaaed99c0ec10225518d0c8d9fb1d2af701e87eb68c', + INSERT INTO oauth_applications (id, owner_type, owner_id, name, uid, secret, redirect_uri, scopes, confidential, created_at, updated_at) + VALUES (3, 'User', 1, 'App 1', 'dHKmvGkmuoMjqhCNmTJkf-EcnA61Up34O1vOHwTSvU8', '965136b8fb8d00e2faa2faaaed99c0ec10225518d0c8d9fb1d2af701e87eb68c', 'http://demo.localhost:3000', 'write_api read_gpx', false, '2021-04-12 17:53:30', '2021-04-12 17:53:30'); - INSERT INTO public.oauth_access_tokens (id, resource_owner_id, application_id, token, refresh_token, expires_in, revoked_at, created_at, scopes, previous_refresh_token) + INSERT INTO public.oauth_access_tokens (id, resource_owner_id, application_id, token, refresh_token, expires_in, revoked_at, created_at, scopes, previous_refresh_token) VALUES (67, 1, 3, '4f41f2328befed5a33bcabdf14483081c8df996cbafc41e313417776e8fafae8', NULL, NULL, NULL, '2021-04-14 19:38:21', 'write_api', ''); - + )" ); @@ -2501,7 +2569,7 @@ TEST_CASE_METHOD( DatabaseTestsFixture, "test_osmchange_rate_limiter", "[changes // Real database function is managed outside of CGImap tdb.run_sql(R"( - + CREATE OR REPLACE FUNCTION api_rate_limit(user_id bigint) RETURNS integer AS $$ @@ -2516,12 +2584,12 @@ TEST_CASE_METHOD( DatabaseTestsFixture, "test_osmchange_rate_limiter", "[changes SELECT COALESCE(SUM(changesets.num_changes), 0) INTO STRICT recent_changes FROM changesets WHERE changesets.user_id = api_rate_limit.user_id AND changesets.created_at >= CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - '1 hour'::interval; - + RETURN max_changes - recent_changes; END IF; END; $$ LANGUAGE plpgsql STABLE; - + )"); } diff --git a/test/test_formatter.cpp b/test/test_formatter.cpp index c32dbae8..87f19c4b 100644 --- a/test/test_formatter.cpp +++ b/test/test_formatter.cpp @@ -152,6 +152,9 @@ void test_formatter::start_document( void test_formatter::end_document() { } +void test_formatter::start_diffresult() { +} + void test_formatter::write_bounds(const bbox &bounds) { } diff --git a/test/test_formatter.hpp b/test/test_formatter.hpp index a6d3a502..239eec80 100644 --- a/test/test_formatter.hpp +++ b/test/test_formatter.hpp @@ -89,6 +89,7 @@ struct test_formatter : public output_formatter { mime::type mime_type() const override; void start_document(const std::string &generator, const std::string &root_name) override; void end_document() override; + void start_diffresult() override; void write_bounds(const bbox &bounds) override; void start_element_type(element_type type) override; void end_element_type(element_type type) override; diff --git a/test/test_parse_osmchange_json_input.cpp b/test/test_parse_osmchange_json_input.cpp new file mode 100644 index 00000000..3c7d46a2 --- /dev/null +++ b/test/test_parse_osmchange_json_input.cpp @@ -0,0 +1,867 @@ +/** + * SPDX-License-Identifier: GPL-2.0-only + * + * This file is part of openstreetmap-cgimap (https://github.com/zerebubuth/openstreetmap-cgimap/). + * + * Copyright (C) 2009-2023 by the CGImap developer community. + * For a full list of authors see the git log. + */ + + +#include "cgimap/options.hpp" +#include "cgimap/api06/changeset_upload/osmchange_json_input_format.hpp" +#include "cgimap/api06/changeset_upload/parser_callback.hpp" +#include "cgimap/util.hpp" +#include "cgimap/http.hpp" + +#include +#include +#include +#include +#include + +#define CATCH_CONFIG_MAIN +#include + +class Test_Parser_Callback : public api06::Parser_Callback { + +public: + Test_Parser_Callback() = default; + + void start_document() override { start_executed = true; } + + void end_document() override { + end_executed = true; + REQUIRE(nodes.empty()); + REQUIRE(ways.empty()); + REQUIRE(relations.empty()); + } + + void process_node(const api06::Node &n, operation op, bool if_unused) override { + REQUIRE(!nodes.empty()); + + auto const& [n_expected, op_expected, if_unused_expected] = nodes.front(); + + REQUIRE(n_expected == n); + REQUIRE(op == op_expected); + REQUIRE(if_unused == if_unused_expected); + + nodes.pop_front(); + } + + void process_way(const api06::Way &w, operation op, bool if_unused) override { + REQUIRE(!ways.empty()); + + auto const& [w_expected, op_expected, if_unused_expected] = ways.front(); + + REQUIRE(w_expected == w); + REQUIRE(op == op_expected); + REQUIRE(if_unused == if_unused_expected); + + ways.pop_front(); + } + + void process_relation(const api06::Relation &r, operation op, bool if_unused) override { + REQUIRE(!relations.empty()); + + auto const& [r_expected, op_expected, if_unused_expected] = relations.front(); + + REQUIRE(r_expected == r); + REQUIRE(op == op_expected); + REQUIRE(if_unused == if_unused_expected); + + relations.pop_front(); + } + + bool start_executed{false}; + bool end_executed{false}; + + using node_tuple = std::tuple; + using way_tuple = std::tuple; + using relation_tuple = std::tuple; + + std::list< node_tuple > nodes; + std::list< way_tuple> ways; + std::list< relation_tuple > relations; +}; + +class global_settings_test_class : public global_settings_default { + +public: + + std::optional get_relation_max_members() const override { + return m_relation_max_members; + } + + std::optional get_element_max_tags() const override { + return m_element_max_tags; + } + + std::optional m_relation_max_members{}; + std::optional m_element_max_tags{}; + +}; + +std::string repeat(const std::string &input, size_t num) { + std::ostringstream os; + std::fill_n(std::ostream_iterator(os), num, input); + return os.str(); +} + +void process_testmsg(const std::string &payload, Test_Parser_Callback& cb) { + + api06::OSMChangeJSONParser parser(cb); + parser.process_message(payload); + + REQUIRE(cb.start_executed); + REQUIRE(cb.end_executed); +} + +void process_testmsg(const std::string &payload) { + + Test_Parser_Callback cb{}; + process_testmsg(payload, cb); +} + + +// OSMCHANGE STRUCTURE TESTS + +TEST_CASE("Invalid JSON", "[osmchange][json]") { + auto i = GENERATE(R"({})", R"(bla)"); + REQUIRE_THROWS_AS(process_testmsg(i), http::bad_request); +} +/* + +TEST_CASE("XML without any changes", "[osmchange][json]") { + REQUIRE_NOTHROW(process_testmsg(R"()")); +} + +TEST_CASE("Invalid XML: osmchange end only", "[osmchange][json]") { + REQUIRE_THROWS_AS(process_testmsg(R"()"), http::bad_request); +} + +TEST_CASE("Misspelled osmchange xml", "[osmchange][json]") { + REQUIRE_THROWS_AS(process_testmsg(R"()"), http::bad_request); +} + +TEST_CASE("osmchange: Unknown action", "[osmchange][json]") { + REQUIRE_THROWS_MATCHES(process_testmsg(R"()"), http::bad_request, + Catch::Message("Unknown action dummy, choices are create, modify, delete at line 1, column 18")); +} + +TEST_CASE("osmchange: Empty create action", "[osmchange][json]") { + REQUIRE_NOTHROW(process_testmsg(R"()")); +} + +TEST_CASE("osmchange: Empty modify action", "[osmchange][json]") { + REQUIRE_NOTHROW(process_testmsg(R"()")); +} + +TEST_CASE("osmchange: Empty delete action", "[osmchange][json]") { + REQUIRE_NOTHROW(process_testmsg(R"()")); +} + +TEST_CASE("osmchange: create invalid object", "[osmchange][json]") { + REQUIRE_THROWS_MATCHES(process_testmsg(R"()"), http::bad_request, + Catch::Message("Unknown element bla, expecting node, way or relation at line 1, column 24")); +} + +*/ + +// NODE TESTS + +TEST_CASE("Create empty node without details", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg(R"({"osmChange": [{ "type": "node", "action": "create"}]})"), http::bad_request); +} + +TEST_CASE("Create node, details except changeset info missing", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg(R"({"osmChange": [{ "type": "node", "action": "create", changeset: 1}]})"), http::bad_request); +} + +TEST_CASE("Create node, lat lon missing", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg(R"({"osmChange": [{ "type": "node", "action": "create", changeset: 12, id: -1}]})"), http::bad_request); +} + +/* +TEST_CASE("Create node, lat missing", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg(R"()"), http::bad_request); +} + +TEST_CASE("Create node, lon missing", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg(R"()"), http::bad_request); +} + +TEST_CASE("Create node, lat outside range", "[osmchange][node][json]") { + auto i = GENERATE(R"(90.01)", R"(-90.01)"); + REQUIRE_THROWS_AS(process_testmsg(fmt::format(R"()", i)), http::bad_request); +} + +TEST_CASE("Create node, lon outside range", "[osmchange][node][json]") { + auto i = GENERATE(R"(180.01)", R"(-180.01)"); + REQUIRE_THROWS_AS(process_testmsg(fmt::format(R"()", i)), http::bad_request); +} + +TEST_CASE("Create node, lat float overflow", "[osmchange][node][json]") { + auto i = GENERATE(R"(9999999999999999999999999999999999999999999999.01)", R"(-9999999999999999999999999999999999999999999999.01)"); + REQUIRE_THROWS_AS(process_testmsg(fmt::format(R"()", i)), http::bad_request); +} + +TEST_CASE("Create node, lon float overflow", "[osmchange][node][json]") { + auto i = GENERATE(R"(9999999999999999999999999999999999999999999999.01)", R"(-9999999999999999999999999999999999999999999999.01)"); + REQUIRE_THROWS_AS(process_testmsg(fmt::format(R"()", i)), http::bad_request); +} + +TEST_CASE("Create node, lat non-finite float", "[osmchange][node][json]") { + auto i = GENERATE(R"(nan)", R"(-nan)", R"(Inf), R"(-Inf)"); + REQUIRE_THROWS_AS(process_testmsg(fmt::format(R"()", i)), http::bad_request); +} + +TEST_CASE("Create node, lon non-finite float", "[osmchange][node][json]") { + auto i = GENERATE(R"(nan)", R"(-nan)", R"(Inf), R"(-Inf)"); + REQUIRE_THROWS_AS(process_testmsg(fmt::format(R"()", i)), http::bad_request); +} + +TEST_CASE("Create node, changeset missing", "[osmchange][node][json]") { + REQUIRE_THROWS_MATCHES(process_testmsg(R"()"), http::bad_request, + Catch::Message("Changeset id is missing for Node -1 at line 1, column 60")); +} + +TEST_CASE("Create node, redefined lat attribute", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg(R"()"), http::bad_request); +} + +TEST_CASE("Create valid node", "[osmchange][node][json]") { + auto i = GENERATE(R"()", + R"()"); + REQUIRE_NOTHROW(process_testmsg(i)); +} + +TEST_CASE("Modify node, missing version", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg(R"()"), http::bad_request); +} + +TEST_CASE("Modify node, invalid version", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg(R"()"), http::bad_request); +} + +TEST_CASE("Delete node", "[osmchange][node][json]") { + REQUIRE_NOTHROW(process_testmsg(R"()")); +} + +TEST_CASE("Delete node, if-unused", "[osmchange][node][json]") { + REQUIRE_NOTHROW(process_testmsg(R"()")); +} + +TEST_CASE("Delete node, missing version", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg(R"()"), http::bad_request); +} + +TEST_CASE("Delete node, invalid version", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg(R"()"), http::bad_request); +} + +TEST_CASE("Delete node, missing id", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg(R"()"), http::bad_request); +} + +TEST_CASE("Create node, extra xml nested inside tag", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"( + )"), http::bad_request); +} + +TEST_CASE("Create node, empty tag key", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"( + )"), http::bad_request); +} + +TEST_CASE("Create node, empty tag value", "[osmchange][node][json]") { + REQUIRE_NOTHROW(process_testmsg( + R"( + )")); +} + +TEST_CASE("Create node, duplicate key dup1", "[osmchange][node][json]") { + REQUIRE_THROWS_MATCHES(process_testmsg( + R"( + + + + + )"), + http::bad_request, Catch::Message("Node -1 has duplicate tags with key dup1 at line 4, column 48")); +} + +TEST_CASE("Create node, tag without value", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"( + )"), http::bad_request); +} + +TEST_CASE("Create node, tag without key", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"( + )"), http::bad_request); +} + +TEST_CASE("Create node, tag value with <= 255 unicode characters", "[osmchange][node][json]") { + for (int i = 0; i <= 255; i++) { + auto v = repeat("😎", i); + REQUIRE_NOTHROW(process_testmsg( + fmt::format(R"( + )", v))); + } +} + +TEST_CASE("Create node, tag value with > 255 unicode characters", "[osmchange][node][json]") { + REQUIRE_THROWS_MATCHES(process_testmsg( + fmt::format(R"( + )", repeat("😎", 256))), + http::bad_request, Catch::Message("Value has more than 255 unicode characters in Node -1 at line 2, column 301")); +} + +TEST_CASE("Create node, tag key with <= 255 unicode characters", "[osmchange][node][json]") { + for (int i = 1; i <= 255; i++) { + auto v = repeat("😎", i); + REQUIRE_NOTHROW(process_testmsg( + fmt::format(R"( + )", v))); + } +} + +TEST_CASE("Create node, tag key with > 255 unicode characters", "[osmchange][node][json]") { + REQUIRE_THROWS_MATCHES(process_testmsg( + fmt::format(R"( + )", repeat("😎", 256))), + http::bad_request, Catch::Message("Key has more than 255 unicode characters in Node -1 at line 2, column 303")); +} + + +// NODE: INVALID ARGUMENTS, OUT OF RANGE VALUES + +TEST_CASE("Modify node, invalid version number", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Modify node, version too large", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Modify node, version negative", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create node, invalid changeset number", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create node, changeset number too large", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create node, changeset number zero", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create node, changeset number negative", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create node, longitude not numeric", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create node, latitude not numeric", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create node, invalid id", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create node, id too large", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create node, id zero", "[osmchange][node][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + + + +// WAY TESTS + +TEST_CASE("Create way, no details", "[osmchange][way][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create way, only changeset", "[osmchange][way][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create way, missing changeset", "[osmchange][way][json]") { + REQUIRE_THROWS_MATCHES(process_testmsg( + R"()"), + http::bad_request, Catch::Message("Changeset id is missing for Way -1 at line 1, column 32")); +} + +TEST_CASE("Create way, missing node ref", "[osmchange][way][json]") { + REQUIRE_THROWS_MATCHES(process_testmsg( + R"()"), + http::precondition_failed, Catch::Message("Precondition failed: Way -1 must have at least one node")); +} + +TEST_CASE("Create way, node refs < max way nodes", "[osmchange][way][json]") { + std::string node_refs{}; + for (uint32_t i = 1; i <= global_settings::get_way_max_nodes(); i++) { + node_refs += fmt::format(R"()", i); + REQUIRE_NOTHROW(process_testmsg( + fmt::format(R"({})", node_refs))); + } +} + +TEST_CASE("Create way, node refs >= max way nodes", "[osmchange][way][json]") { + std::string node_refs{}; + for (uint32_t i = 1; i <= global_settings::get_way_max_nodes(); i++) + node_refs += fmt::format(R"()", i); + for (uint32_t j = global_settings::get_way_max_nodes()+1; j < global_settings::get_way_max_nodes() + 10; ++j) { + node_refs += fmt::format(R"()", j); + REQUIRE_THROWS_MATCHES(process_testmsg( + fmt::format(R"({})", node_refs)), + http::bad_request, Catch::Message(fmt::format("You tried to add {} nodes to way -1, however only {} are allowed", j, global_settings::get_way_max_nodes()))); + } +} + +TEST_CASE("Create way, with tags", "[osmchange][way][json]") { + REQUIRE_NOTHROW(process_testmsg( + R"()")); +} + +TEST_CASE("Create way, node ref not numeric", "[osmchange][way][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create way, node ref too large", "[osmchange][way][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create way, invalid zero node ref", "[osmchange][way][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create way, node ref missing", "[osmchange][way][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Delete way, no version", "[osmchange][way][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), + http::bad_request); +} + +TEST_CASE("Delete way, no id", "[osmchange][way][json]") { + REQUIRE_THROWS_MATCHES(process_testmsg( + R"()"), + http::bad_request, Catch::Message(fmt::format("Mandatory field id missing in object at line 1, column 52"))); +} + +TEST_CASE("Delete way, no changeset", "[osmchange][way][json]") { + REQUIRE_THROWS_MATCHES(process_testmsg( + R"()"), + http::bad_request, Catch::Message(fmt::format("Changeset id is missing for Way -1 at line 1, column 44"))); +} + +TEST_CASE("Delete way", "[osmchange][way][json]") { + REQUIRE_NOTHROW(process_testmsg(R"()")); +} + + +// RELATION TESTS + +TEST_CASE("Create relation, id missing", "[osmchange][relation][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create relation, member ref missing", "[osmchange][relation][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create relation, no member role", "[osmchange][relation][json]") { + REQUIRE_NOTHROW(process_testmsg( + R"()")); +} + +TEST_CASE("Create relation, member type missing", "[osmchange][relation][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create relation, invalid member type", "[osmchange][relation][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create relation, invalid member ref", "[osmchange][relation][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create relation, invalid member ref zero", "[osmchange][relation][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"()"), http::bad_request); +} + +TEST_CASE("Create relation, member ref too large", "[osmchange][relation][json]") { + REQUIRE_THROWS_AS(process_testmsg( + R"( + + )"), http::bad_request); +} + +TEST_CASE("Create relation, role with <= 255 unicode characters", "[osmchange][relation][json]") { + for (int i = 1; i <= 255; i++) { + auto v = repeat("😎", i); + REQUIRE_NOTHROW(process_testmsg( + fmt::format( + R"( + + )", + v))); + } +} + +TEST_CASE("Create relation, role with > 255 unicode characters", "[osmchange][relation][json]") { + REQUIRE_THROWS_MATCHES(process_testmsg( + fmt::format( + R"( + + )", + repeat("😎", 256))), + http::bad_request, Catch::Message("Relation Role has more than 255 unicode characters at line 2, column 321")); +} + +TEST_CASE("Delete relation, no version", "[osmchange][relation][json]") { + REQUIRE_THROWS_MATCHES(process_testmsg( + R"()"), + http::bad_request, Catch::Message(fmt::format("Version is required when updating Relation -1 at line 1, column 53"))); +} + +TEST_CASE("Delete relation, no id", "[osmchange][relation][json]") { + REQUIRE_THROWS_MATCHES(process_testmsg( + R"()"), + http::bad_request, Catch::Message(fmt::format("Mandatory field id missing in object at line 1, column 57"))); +} + +TEST_CASE("Delete relation", "[osmchange][relation][json]") { + REQUIRE_NOTHROW(process_testmsg( + R"()")); +} + +// INVALID DATA TESTS + +TEST_CASE("Invalid data", "[osmchange][json]") { + REQUIRE_THROWS_AS(process_testmsg("\x3C\x00\x00\x00\x00\x0A\x01\x00"), http::bad_request); +} + +*/ + +// LARGE MESSAGE TESTS + +TEST_CASE("Very large JSON message", "[osmchange][node][json]") { + + // Test JSON processing with a very large message + std::stringstream s; + + s << R"( + { + "version": "0.6", + "generator": "demo", + "osmChange": [ + )"; + + Test_Parser_Callback cb{}; + + for (int i = 1; i < 100000; i++) { + + if (i > 1) { + s << ",\n"; + } + + api06::Node node; + node.set_id(-i); + node.set_changeset(123); + node.add_tags({{"some key", "some value"}}); + + switch (i % 3) { + case 0: + node.set_lat(1); + node.set_lon(2); + node.set_version(0); // operation create forces version 0, regardless of JSON contents + + cb.nodes.emplace_back(node, operation::op_create, false); + + s << fmt::format(R"( + {{ + "type": "node", + "action": "{}", + "id": {}, + "lat": 1, + "lon": 2, + "changeset": 123, + "tags": {{ + "some key": "some value" + }} + }} + )", "create", -i); + + break; + + case 1: + node.set_lat(1); + node.set_lon(2); + node.set_version(1); + + cb.nodes.emplace_back(node, operation::op_modify, false); + + s << fmt::format(R"( + {{ + "type": "node", + "action": "{}", + "id": {}, + "lat": 1, + "lon": 2, + "version": 1, + "changeset": 123, + "tags": {{ + "some key": "some value" + }} + }} + )", "modify", -i); + break; + + case 2: + node.set_version(1); + cb.nodes.emplace_back(node, operation::op_delete, false); + + s << fmt::format(R"( + {{ + "type": "node", + "action": "{}", + "id": {}, + "version": 1, + "changeset": 123, + "tags": {{ + "some key": "some value" + }} + }} + )", "delete", -i); + + break; + + } + } + + s << R"( + ] + } + )"; + + REQUIRE_NOTHROW(process_testmsg(s.str(), cb)); + +} + +/* + +// OBJECT LIMIT TESTS + +TEST_CASE("Create node, tags < max tags", "[osmchange][node][json]") { + auto test_settings = std::unique_ptr(new global_settings_test_class()); + test_settings->m_element_max_tags = 50; + + global_settings::set_configuration(std::move(test_settings)); + REQUIRE(global_settings::get_element_max_tags()); + + std::string tags{}; + for (uint32_t i = 1; i <= global_settings::get_element_max_tags(); i++) { + tags += fmt::format("", i); + REQUIRE_NOTHROW(process_testmsg( + fmt::format(R"({})", tags))); + } +} + +TEST_CASE("Create node, tags >= max tags", "[osmchange][node][json]") { + auto test_settings = std::unique_ptr(new global_settings_test_class()); + test_settings->m_element_max_tags = 50; + + global_settings::set_configuration(std::move(test_settings)); + REQUIRE(global_settings::get_element_max_tags()); + + std::string tags{}; + for (uint32_t i = 1; i <= *global_settings::get_element_max_tags(); i++) + tags += fmt::format("", i); + for (uint32_t j = *global_settings::get_element_max_tags()+1; j < *global_settings::get_element_max_tags() + 10; ++j) { + tags += fmt::format("", j); + REQUIRE_THROWS_AS(process_testmsg( + fmt::format(R"({})", tags)), + http::bad_request); + } +} + +TEST_CASE("Create relation, members < max members", "[osmchange][relation][json]") { + auto test_settings = std::unique_ptr(new global_settings_test_class()); + test_settings->m_relation_max_members = 32000; + + global_settings::set_configuration(std::move(test_settings)); + REQUIRE(global_settings::get_relation_max_members()); + + std::string members = repeat(R"()", *global_settings::get_relation_max_members()); + REQUIRE_NOTHROW(process_testmsg( + fmt::format(R"({}")", members))); +} + +TEST_CASE("Create relation, members >= max members", "[osmchange][relation][json]") { + auto test_settings = std::unique_ptr(new global_settings_test_class()); + test_settings->m_relation_max_members = 32000; + + global_settings::set_configuration(std::move(test_settings)); + REQUIRE(global_settings::get_relation_max_members()); + + std::string members = repeat(R"()", *global_settings::get_relation_max_members()); + for (uint32_t j = *global_settings::get_relation_max_members()+1; j < *global_settings::get_relation_max_members() + 3; ++j) { + members += R"()"; + REQUIRE_THROWS_AS(process_testmsg( + fmt::format(R"({}")", members)), + http::bad_request); + } +} +*/ + +TEST_CASE("Create node", "[osmchange][node][json]") { + + Test_Parser_Callback cb{}; + api06::Node node; + node.set_id(-1); + node.set_lat(42.7957187); + node.set_lon(13.5690032); + node.set_changeset(124176968); + node.set_version(0); // operation create forces version 0, regardless of JSON contents + node.add_tags({{"man_made", "mast"},{"name", "Monte Piselli - San Giacomo"}}); + + cb.nodes.emplace_back(node, operation::op_create, false); + + REQUIRE_NOTHROW(process_testmsg( + R"( + { + "version": "0.6", + "generator": "demo", + "osmChange": [ + { + "type": "node", + "action": "create", + "id": -1, + "lat": 42.7957187, + "lon": 13.5690032, + "changeset": 124176968, + "tags": { + "man_made": "mast", + "name": "Monte Piselli - San Giacomo" + } + } + ] + } + )", cb)); +} + +TEST_CASE("Create way", "[osmchange][way][json]") { + + Test_Parser_Callback cb{}; + api06::Way way; + way.set_id(-1); + way.set_changeset(124176968); + way.set_version(0); // operation create forces version 0, regardless of JSON contents + way.add_way_nodes({1,2,3,4}); + way.add_tags({{"highway", "residential"},{"name", "Via Monte"}}); + + cb.ways.emplace_back(way, operation::op_create, false); + + REQUIRE_NOTHROW(process_testmsg( + R"( + { + "version": "0.6", + "generator": "demo", + "osmChange": [ + { + "type": "way", + "action": "create", + "id": -1, + "changeset": 124176968, + "nodes": [1,2,3,4], + "tags": { + "highway": "residential", + "name": "Via Monte" + } + } + ] + } + )", cb)); +} + +TEST_CASE("Create relation", "[osmchange][relation][json]") { + + Test_Parser_Callback cb{}; + api06::Relation rel; + rel.set_id(-1); + rel.set_changeset(124176968); + rel.set_version(0); // operation create forces version 0, regardless of JSON contents + rel.add_tags({{"route", "bus"}, {"ref", "23"}}); // last ref tag wins + rel.add_members({{"Node", -1, "stop"}, {"Way", -2, ""}, {"Relation", -3, "parent"}}); + + cb.relations.emplace_back(rel, operation::op_create, false); + + REQUIRE_NOTHROW(process_testmsg( + R"( + { + "version": "0.6", + "generator": "demo", + "osmChange": [ + { + "type": "relation", + "action": "create", + "id": -1, + "changeset": 124176968, + "members": [ + {"type": "Node", "ref": -1, "role": "stop"}, + {"type": "Way", "ref": -2}, + {"type": "Relation", "ref": -3, "role": "parent"} + ], + "tags": { + "ref": "123", + "route": "bus", + "ref": "23" + } + } + ] + } + )", cb)); +} diff --git a/test/test_parse_osmchange_input.cpp b/test/test_parse_osmchange_xml_input.cpp similarity index 98% rename from test/test_parse_osmchange_input.cpp rename to test/test_parse_osmchange_xml_input.cpp index 6fe53236..9a838d34 100644 --- a/test/test_parse_osmchange_input.cpp +++ b/test/test_parse_osmchange_xml_input.cpp @@ -9,7 +9,7 @@ #include "cgimap/options.hpp" -#include "cgimap/api06/changeset_upload/osmchange_input_format.hpp" +#include "cgimap/api06/changeset_upload/osmchange_xml_input_format.hpp" #include "cgimap/api06/changeset_upload/parser_callback.hpp" #include "cgimap/util.hpp" #include "cgimap/http.hpp" @@ -92,7 +92,7 @@ TEST_CASE("Misspelled osmchange xml", "[osmchange][xml]") { } TEST_CASE("osmchange: Unknown action", "[osmchange][xml]") { - REQUIRE_THROWS_MATCHES(process_testmsg(R"()"), http::bad_request, + REQUIRE_THROWS_MATCHES(process_testmsg(R"()"), http::bad_request, Catch::Message("Unknown action dummy, choices are create, modify, delete at line 1, column 18")); } @@ -109,7 +109,7 @@ TEST_CASE("osmchange: Empty delete action", "[osmchange][xml]") { } TEST_CASE("osmchange: create invalid object", "[osmchange][xml]") { - REQUIRE_THROWS_MATCHES(process_testmsg(R"()"), http::bad_request, + REQUIRE_THROWS_MATCHES(process_testmsg(R"()"), http::bad_request, Catch::Message("Unknown element bla, expecting node, way or relation at line 1, column 24")); } @@ -168,7 +168,7 @@ TEST_CASE("Create node, lon non-finite float", "[osmchange][node][xml]") { } TEST_CASE("Create node, changeset missing", "[osmchange][node][xml]") { - REQUIRE_THROWS_MATCHES(process_testmsg(R"()"), http::bad_request, + REQUIRE_THROWS_MATCHES(process_testmsg(R"()"), http::bad_request, Catch::Message("Changeset id is missing for Node -1 at line 1, column 60")); } @@ -177,7 +177,7 @@ TEST_CASE("Create node, redefined lat attribute", "[osmchange][node][xml]") { } TEST_CASE("Create valid node", "[osmchange][node][xml]") { - auto i = GENERATE(R"()", + auto i = GENERATE(R"()", R"()"); REQUIRE_NOTHROW(process_testmsg(i)); } @@ -235,7 +235,7 @@ TEST_CASE("Create node, duplicate key dup1", "[osmchange][node][xml]") { - )"), + )"), http::bad_request, Catch::Message("Node -1 has duplicate tags with key dup1 at line 4, column 48")); } @@ -263,7 +263,7 @@ TEST_CASE("Create node, tag value with <= 255 unicode characters", "[osmchange][ TEST_CASE("Create node, tag value with > 255 unicode characters", "[osmchange][node][xml]") { REQUIRE_THROWS_MATCHES(process_testmsg( fmt::format(R"( - )", repeat("😎", 256))), + )", repeat("😎", 256))), http::bad_request, Catch::Message("Value has more than 255 unicode characters in Node -1 at line 2, column 301")); } @@ -279,7 +279,7 @@ TEST_CASE("Create node, tag key with <= 255 unicode characters", "[osmchange][no TEST_CASE("Create node, tag key with > 255 unicode characters", "[osmchange][node][xml]") { REQUIRE_THROWS_MATCHES(process_testmsg( fmt::format(R"( - )", repeat("😎", 256))), + )", repeat("😎", 256))), http::bad_request, Catch::Message("Key has more than 255 unicode characters in Node -1 at line 2, column 303")); } @@ -378,13 +378,13 @@ TEST_CASE("Create way, only changeset", "[osmchange][way][xml]") { TEST_CASE("Create way, missing changeset", "[osmchange][way][xml]") { REQUIRE_THROWS_MATCHES(process_testmsg( - R"()"), + R"()"), http::bad_request, Catch::Message("Changeset id is missing for Way -1 at line 1, column 32")); } TEST_CASE("Create way, missing node ref", "[osmchange][way][xml]") { REQUIRE_THROWS_MATCHES(process_testmsg( - R"()"), + R"()"), http::precondition_failed, Catch::Message("Precondition failed: Way -1 must have at least one node")); } @@ -519,7 +519,7 @@ TEST_CASE("Create relation, role with > 255 unicode characters", "[osmchange][re R"( )", - repeat("😎", 256))), + repeat("😎", 256))), http::bad_request, Catch::Message("Relation Role has more than 255 unicode characters at line 2, column 321")); } @@ -531,7 +531,7 @@ TEST_CASE("Delete relation, no version", "[osmchange][relation][xml]") { TEST_CASE("Delete relation, no id", "[osmchange][relation][xml]") { REQUIRE_THROWS_MATCHES(process_testmsg( - R"()"), + R"()"), http::bad_request, Catch::Message(fmt::format("Mandatory field id missing in object at line 1, column 57"))); }