diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 66c1870b8..96b9a9f97 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -51,7 +51,7 @@ jobs: # checks-out your repository under $GITHUB_WORKSPACE # see https://github.com/actions/checkout - name: Checkout Agent - uses: actions/checkout@v3 + uses: actions/checkout@v4 # the QEMU emulator lets us build for arm processors also # see https://github.com/docker/setup-qemu-action diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 11bedd164..4a7ebb5c4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,11 +15,11 @@ on: # Enable automated build once verified pull_request: paths-ignore: ["**/*.md", "LICENSE.txt", ".gitignore"] - branches: [ "main", "main-dev" ] + branches: [ "main" ] push: paths-ignore: ["**/*.md", "LICENSE.txt", ".gitignore"] - branches: [ "main", "main-dev" ] + branches: [ "main" ] tags: - "v*.*.*" @@ -64,11 +64,11 @@ jobs: echo $CTEST_OUTPUT_ON_FAILURE - name: Checkout Agent - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Cache conan packages + - name: Restore cached conan packages id: cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ env.CONAN_HOME }} key: ${{ runner.os }}-build-${{ matrix.profile }}-${{ hashFiles('**/conanfile.py') }} @@ -98,6 +98,12 @@ jobs: set CTEST_OUTPUT_ON_FAILURE=TRUE conan create . --build=missing -pr conan/profiles/${{ matrix.profile }} -o "&:with_docs=False" -o "&:cpack=True" -o "&:cpack_destination=${{ env.ZIP_DIR }}" -o "&:shared=${{ matrix.shared }}" + - name: Clean conan cache + continue-on-error: true + run: | + conan remove mtconnect_agent -c + conan cache clean "*" --source --build --download + - name: Release uses: softprops/action-gh-release@v2 if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.shared == 'False' }} @@ -123,15 +129,15 @@ jobs: sudo apt install -y build-essential cmake gcc-11 g++-11 python3 autoconf automake - name: Checkout Agent - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Cache conan packages + - name: Restore cached conan packages id: cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.conan2 key: ${{ runner.os }}-build-${{ matrix.shared }}-${{ hashFiles('**/conanfile.py') }} - + - name: Install Conan uses: turtlebrowser/get-conan@v1.2 @@ -152,7 +158,14 @@ jobs: export CTEST_OUTPUT_ON_FAILURE=TRUE conan create . --build=missing -pr conan/profiles/gcc -o '&:shared=${{ matrix.shared }}' -o '&:with_docs=False' -o '&:cpack=True' -o '&:cpack_name=dist' -o '&:cpack_destination=${{ github.workspace }}' - - name: Cleanse package version + - name: Clean conan cache + continue-on-error: true + run: | + conan remove mtconnect_agent -c + conan cache clean "*" --source --build --download + + - name: Derive Package Version + if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.shared == 'False' }} run: | PACKAGE_VERSION=${{ github.ref_name }} if [[ $PACKAGE_VERSION == v*.*.*.* ]]; then @@ -162,7 +175,9 @@ jobs: else echo "PACKAGE_VERSION=0.0.0.0" >> $GITHUB_ENV fi + - name: Prepare Debian Package + if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.shared == 'False' }} shell: bash working-directory: ${{ github.workspace }} run: | @@ -170,7 +185,9 @@ jobs: ls -lah pkgroot tar -xzf dist.tar.gz -C pkgroot/ mv pkgroot/dist pkgroot/usr + - name: Create Debian Package + if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.shared == 'False' }} id: create_debian_package uses: jiro4989/build-deb-action@v3 with: @@ -180,9 +197,10 @@ jobs: arch: amd64 desc: "MTConnect Agent for Ununtu" maintainer: Datanomix + - name: Release - uses: softprops/action-gh-release@v2 if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.shared == 'False' }} + uses: softprops/action-gh-release@v2 with: name: Version ${{ github.ref_name }} draft: true @@ -199,15 +217,15 @@ jobs: steps: - name: Checkout Agent - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Cache conan packages + - name: Restore cached conan packages id: cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.conan2 key: ${{ runner.os }}-build-${{ matrix.shared }}-${{ hashFiles('**/conanfile.py') }} - + - name: Install Conan run: | brew install conan @@ -228,4 +246,9 @@ jobs: run: | export CTEST_OUTPUT_ON_FAILURE=TRUE conan create . --build=missing -pr conan/profiles/macos -o '&:shared=${{ matrix.shared }}' -o '&:with_docs=False' - + + - name: Clean conan cache + continue-on-error: true + run: | + conan remove mtconnect_agent -c + conan cache clean "*" --source --build --download diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ef5906ab..c1590e5f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ set(AGENT_VERSION_MAJOR 2) set(AGENT_VERSION_MINOR 7) set(AGENT_VERSION_PATCH 0) -set(AGENT_VERSION_BUILD 5) +set(AGENT_VERSION_BUILD 7) set(AGENT_VERSION_RC "") # This minimum version is to support Visual Studio 2019 and C++ feature checking and FetchContent @@ -27,7 +27,7 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(CXX_COMPILE_FEATURES cxx_std_20) -set(CMAKE_OSX_DEPLOYMENT_TARGET 13.3) +set(CMAKE_OSX_DEPLOYMENT_TARGET 15.0) project(cppagent LANGUAGES C CXX) diff --git a/conan/profiles/macos b/conan/profiles/macos index b7c3df9ac..45380ed61 100644 --- a/conan/profiles/macos +++ b/conan/profiles/macos @@ -3,6 +3,7 @@ include(default) [settings] compiler=apple-clang compiler.cppstd=20 +os.version=15.0 [system_tools] cmake/>3.26.0 diff --git a/src/mtconnect/configuration/agent_config.cpp b/src/mtconnect/configuration/agent_config.cpp index ab63fa019..2bcafde47 100644 --- a/src/mtconnect/configuration/agent_config.cpp +++ b/src/mtconnect/configuration/agent_config.cpp @@ -176,8 +176,9 @@ namespace mtconnect::configuration { cerr << "Loading configuration from:" << *path << endl; m_configFile = fs::canonical(*path); - addPathFront(m_configPaths, m_configFile.parent_path()); - addPathBack(m_dataPaths, m_configFile.parent_path()); + m_configPath = m_configFile.parent_path(); + addPathFront(m_configPaths, m_configPath); + addPathBack(m_dataPaths, m_configPath); ifstream file(m_configFile.c_str()); std::stringstream buffer; @@ -528,8 +529,8 @@ namespace mtconnect::configuration { ConfigOptions options; AddDefaultedOptions(logger, options, - {{"max_size", "10mb"s}, - {"rotation_size", "2mb"s}, + {{"max_size", "2mb"s}, + {"max_archive_size", "10mb"s}, {"max_index", 9}, {"file_name", defaultFileName}, {"archive_pattern", defaultArchivePattern}}); @@ -593,15 +594,16 @@ namespace mtconnect::configuration { } } - auto &maxLogFileSize = logChannel.m_maxLogFileSize; + auto &maxLogArchiveSize = logChannel.m_maxLogArchiveSize; auto &logRotationSize = logChannel.m_logRotationSize; auto &rotationLogInterval = logChannel.m_rotationLogInterval; auto &logArchivePattern = logChannel.m_logArchivePattern; auto &logDirectory = logChannel.m_logDirectory; + auto &archiveLogDirectory = logChannel.m_archiveLogDirectory; auto &logFileName = logChannel.m_logFileName; - maxLogFileSize = ConvertFileSize(options, "max_size", maxLogFileSize); - logRotationSize = ConvertFileSize(options, "rotation_size", logRotationSize); + maxLogArchiveSize = ConvertFileSize(options, "max_archive_size", maxLogArchiveSize); + logRotationSize = ConvertFileSize(options, "max_size", logRotationSize); int max_index = *GetOption(options, "max_index"); if (auto sched = GetOption(options, "schedule")) @@ -618,29 +620,51 @@ namespace mtconnect::configuration { auto file_name = *GetOption(options, "file_name"); auto archive_pattern = *GetOption(options, "archive_pattern"); + // Default the log directory to the configuration file path. + logDirectory = m_configPath; + logFileName = fs::path(file_name); logArchivePattern = fs::path(archive_pattern); - if (!logArchivePattern.has_filename()) + + // Determine the log directory based on the provided file name and archive pattern + if (logFileName.is_absolute()) + logDirectory = logFileName.parent_path(); + else if (logArchivePattern.is_absolute()) + logDirectory = logArchivePattern.parent_path(); + + // If the log file name is relative and has a parent path, use it to determine the log directory + if (logFileName.is_relative() && logFileName.has_parent_path()) { - logArchivePattern = logArchivePattern / archiveFileName(get(options["file_name"])); + logDirectory = logDirectory / logFileName.parent_path(); + logFileName = logFileName.filename(); + } + else if (logArchivePattern.is_relative() && logArchivePattern.has_parent_path()) + { + logDirectory = logDirectory / logArchivePattern.parent_path(); + logArchivePattern = logArchivePattern.filename(); } - if (logArchivePattern.is_relative()) - logArchivePattern = fs::current_path() / logArchivePattern; + // Make sure the log archive pattern includes a file name, use the default file name as the + // base. + if (!logArchivePattern.has_filename()) + logArchivePattern = logArchivePattern / archiveFileName(get(options["file_name"])); - // Get the log directory from the archive path. - logDirectory = logArchivePattern.parent_path(); + // Make the logArchivePattern and logFileName absolute paths + logDirectory = logDirectory.lexically_normal(); - // If the file name does not specify a log directory, use the - // archive directory - logFileName = fs::path(file_name); - if (!logFileName.has_parent_path()) + if (logArchivePattern.is_relative()) + logArchivePattern = logDirectory / logArchivePattern; + logArchivePattern = logArchivePattern.lexically_normal(); + archiveLogDirectory = logArchivePattern.parent_path(); + fs::create_directories(archiveLogDirectory); + + if (logFileName.is_relative()) logFileName = logDirectory / logFileName; - else if (logFileName.is_relative()) - logFileName = fs::current_path() / logFileName; + logFileName = logFileName.lexically_normal(); + fs::create_directories(logFileName.parent_path()); // Create a text file sink auto sink = boost::make_shared( - kw::file_name = logFileName, kw::target_file_name = logArchivePattern.filename(), + kw::file_name = logFileName.string(), kw::target_file_name = logArchivePattern.string(), kw::auto_flush = true, kw::rotation_size = logRotationSize, kw::open_mode = ios_base::out | ios_base::app, kw::format = formatter); @@ -649,7 +673,8 @@ namespace mtconnect::configuration { // Set up where the rotated files will be stored sink->locked_backend()->set_file_collector(logr::sinks::file::make_collector( - kw::target = logDirectory, kw::max_size = maxLogFileSize, kw::max_files = max_index)); + kw::target = archiveLogDirectory.string(), kw::max_size = maxLogArchiveSize, + kw::max_files = max_index)); if (rotationLogInterval > 0) { @@ -1248,7 +1273,8 @@ namespace mtconnect::configuration { } catch (exception &e) { - LOG(info) << "Cannot load plugin " << name << " from " << path << " Reason: " << e.what(); + LOG(debug) << "Plugin " << name << " from " << path << " not found, Reason: " << e.what() + << ", trying next path if available."; } } diff --git a/src/mtconnect/configuration/agent_config.hpp b/src/mtconnect/configuration/agent_config.hpp index 742d1261b..65885a7f3 100644 --- a/src/mtconnect/configuration/agent_config.hpp +++ b/src/mtconnect/configuration/agent_config.hpp @@ -183,17 +183,25 @@ namespace mtconnect { { return m_logChannels[channelName].m_logArchivePattern; } + + /// @brief gets the archive log directory + /// @return log directory + const auto &getArchiveLogDirectory(const std::string &channelName = "agent") + { + return m_logChannels[channelName].m_archiveLogDirectory; + } + /// @brief Get the maximum size of all the log files /// @return the maximum size of all log files - auto getMaxLogFileSize(const std::string &channelName = "agent") + auto getLogRotationSize(const std::string &channelName = "agent") { - return m_logChannels[channelName].m_maxLogFileSize; + return m_logChannels[channelName].m_logRotationSize; } /// @brief the maximum size of a log file when it triggers rolling over /// @return the maxumum site of a log file - auto getLogRotationSize(const std::string &channelName = "agent") + auto getMaxLogArchiveSize(const std::string &channelName = "agent") { - return m_logChannels[channelName].m_logRotationSize; + return m_logChannels[channelName].m_maxLogArchiveSize; } /// @brief How often to roll over the log file /// @@ -260,6 +268,10 @@ namespace mtconnect { /// @param path the path to add void addPluginPath(const std::filesystem::path &path) { addPathBack(m_pluginPaths, path); } + ///@brief set the config path for testing + ///@param path the path to set for the config file directory + void setConfigPath(const std::filesystem::path &path) { m_configPath = path; } + protected: DevicePtr getDefaultDevice(); void loadAdapters(const ptree &tree, const ConfigOptions &options); @@ -295,7 +307,7 @@ namespace mtconnect { else { LOG(debug) << "Cannot find file '" << file << "' " - << " in path " << path; + << " in path " << path << ", continuing..."; } } @@ -312,7 +324,7 @@ namespace mtconnect { if (!ec) paths.emplace_back(con); else - LOG(debug) << "Cannot file path: " << path << ", " << ec.message(); + LOG(debug) << "Cannot find path: " << path << ", " << ec.message() << ", skipping..."; } void addPathFront(std::list &paths, std::filesystem::path path) @@ -348,10 +360,11 @@ namespace mtconnect { { std::string m_channelName; std::filesystem::path m_logDirectory; + std::filesystem::path m_archiveLogDirectory; std::filesystem::path m_logArchivePattern; std::filesystem::path m_logFileName; - int64_t m_maxLogFileSize {0}; + int64_t m_maxLogArchiveSize {0}; int64_t m_logRotationSize {0}; int64_t m_rotationLogInterval {0}; @@ -374,6 +387,7 @@ namespace mtconnect { std::string m_devicesFile; std::filesystem::path m_exePath; std::filesystem::path m_working; + std::filesystem::path m_configPath; std::list m_configPaths; std::list m_dataPaths; diff --git a/src/mtconnect/device_model/configuration/coordinate_systems.cpp b/src/mtconnect/device_model/configuration/coordinate_systems.cpp index dcd0007bf..b3fbd47fa 100644 --- a/src/mtconnect/device_model/configuration/coordinate_systems.cpp +++ b/src/mtconnect/device_model/configuration/coordinate_systems.cpp @@ -33,7 +33,8 @@ namespace mtconnect { Requirement("Rotation", ValueType::VECTOR, 3, false), Requirement("TranslationDataSet", ValueType::DATA_SET, false), Requirement("RotationDataSet", ValueType::DATA_SET, false)}); - transformation->setOrder({"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"}); + transformation->setOrder( + {"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"}); auto coordinateSystem = make_shared(Requirements { Requirement("id", true), Requirement("name", false), Requirement("nativeName", false), diff --git a/src/mtconnect/device_model/configuration/motion.cpp b/src/mtconnect/device_model/configuration/motion.cpp index bac6f33f3..7a3d60b22 100644 --- a/src/mtconnect/device_model/configuration/motion.cpp +++ b/src/mtconnect/device_model/configuration/motion.cpp @@ -30,7 +30,8 @@ namespace mtconnect { Requirement("Rotation", ValueType::VECTOR, 3, false), Requirement("TranslationDataSet", ValueType::DATA_SET, false), Requirement("RotationDataSet", ValueType::DATA_SET, false)}); - transformation->setOrder({"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"}); + transformation->setOrder( + {"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"}); static auto motion = make_shared(Requirements { Requirement("id", true), Requirement("parentIdRef", false), diff --git a/src/mtconnect/device_model/configuration/solid_model.cpp b/src/mtconnect/device_model/configuration/solid_model.cpp index 76a5fd662..8d3e4e36c 100644 --- a/src/mtconnect/device_model/configuration/solid_model.cpp +++ b/src/mtconnect/device_model/configuration/solid_model.cpp @@ -29,11 +29,12 @@ namespace mtconnect { if (!solidModel) { static auto transformation = make_shared( - Requirements {Requirement("Translation", ValueType::VECTOR, 3, false), - Requirement("Rotation", ValueType::VECTOR, 3, false), - Requirement("TranslationDataSet", ValueType::DATA_SET, false), - Requirement("RotationDataSet", ValueType::DATA_SET, false)}); - transformation->setOrder({"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"}); + Requirements {Requirement("Translation", ValueType::VECTOR, 3, false), + Requirement("Rotation", ValueType::VECTOR, 3, false), + Requirement("TranslationDataSet", ValueType::DATA_SET, false), + Requirement("RotationDataSet", ValueType::DATA_SET, false)}); + transformation->setOrder( + {"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"}); solidModel = make_shared( Requirements {{"id", true}, diff --git a/src/mtconnect/entity/xml_parser.cpp b/src/mtconnect/entity/xml_parser.cpp index 53cf80e45..a07877fd4 100644 --- a/src/mtconnect/entity/xml_parser.cpp +++ b/src/mtconnect/entity/xml_parser.cpp @@ -287,7 +287,7 @@ namespace mtconnect::entity { } else { - LOG(warning) << "Unexpected element: " << nodeQName(child); + // LOG(warning) << "Unexpected element: " << nodeQName(child); errors.emplace_back( new EntityError("Invalid element '" + nodeQName(child) + "'", qname)); } diff --git a/src/mtconnect/parser/xml_parser.cpp b/src/mtconnect/parser/xml_parser.cpp index 9734d0ad5..3b2511c20 100644 --- a/src/mtconnect/parser/xml_parser.cpp +++ b/src/mtconnect/parser/xml_parser.cpp @@ -202,13 +202,26 @@ namespace mtconnect::parser { { auto device = entity::XmlParser::parseXmlNode(Device::getRoot(), nodeset->nodeTab[i], errors); + if (device) + { deviceList.emplace_back(dynamic_pointer_cast(device)); + } + else + { + LOG(error) << "Failed to parse device, skipping"; + } if (!errors.empty()) { for (auto &e : errors) - LOG(warning) << "Error parsing device: " << e->what(); + { + if (device) + LOG(warning) << "When loading device " << device->get("name") + << ", A problem was skipped: " << e->what(); + else + LOG(error) << "Failed to load device: " << e->what(); + } } } } diff --git a/src/mtconnect/pipeline/shdr_token_mapper.cpp b/src/mtconnect/pipeline/shdr_token_mapper.cpp index efef18c9f..3baf14f3b 100644 --- a/src/mtconnect/pipeline/shdr_token_mapper.cpp +++ b/src/mtconnect/pipeline/shdr_token_mapper.cpp @@ -166,8 +166,8 @@ namespace mtconnect { } catch (entity::PropertyError &e) { - LOG(warning) << "Cannot convert value for data item id '" << dataItem->getId() - << "': " << *token << " - " << e.what(); + LOG(debug) << "Cannot convert value for data item id '" << dataItem->getId() + << "': " << *token << " - " << e.what(); if (schemaVersion >= SCHEMA_VERSION(2, 5) && validation) { props.insert_or_assign("quality", "INVALID"s); diff --git a/src/mtconnect/sink/rest_sink/parameter.hpp b/src/mtconnect/sink/rest_sink/parameter.hpp index 541e4dcd0..e7348d641 100644 --- a/src/mtconnect/sink/rest_sink/parameter.hpp +++ b/src/mtconnect/sink/rest_sink/parameter.hpp @@ -64,6 +64,9 @@ namespace mtconnect::sink::rest_sink { Parameter(const std::string &n, ParameterType t = STRING, UrlPart p = PATH) : m_name(n), m_type(t), m_part(p) {} + Parameter(const std::string_view &n, ParameterType t = STRING, UrlPart p = PATH) + : m_name(n), m_type(t), m_part(p) + {} Parameter(const Parameter &o) = default; /// @brief to support std::set interface diff --git a/src/mtconnect/sink/rest_sink/response.hpp b/src/mtconnect/sink/rest_sink/response.hpp index a380c609a..80bf2356b 100644 --- a/src/mtconnect/sink/rest_sink/response.hpp +++ b/src/mtconnect/sink/rest_sink/response.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include "cached_file.hpp" @@ -64,6 +65,9 @@ namespace mtconnect { std::optional m_requestId; ///< Request id from websocket sub CachedFilePtr m_file; ///< Cached file if a file is being returned + + /// @brief Additional per-response header fields (e.g. for CORS preflight) + std::list> m_fields; }; using ResponsePtr = std::unique_ptr; diff --git a/src/mtconnect/sink/rest_sink/routing.hpp b/src/mtconnect/sink/rest_sink/routing.hpp index f3ee2f37a..709d13e4f 100644 --- a/src/mtconnect/sink/rest_sink/routing.hpp +++ b/src/mtconnect/sink/rest_sink/routing.hpp @@ -17,8 +17,10 @@ #pragma once +#include #include +#include #include #include #include @@ -84,7 +86,8 @@ namespace mtconnect::sink::rest_sink { m_pattern(pattern), m_command(request), m_function(function), - m_swagger(swagger) + m_swagger(swagger), + m_catchAll(true) {} /// @brief Added summary and description to the routing @@ -295,6 +298,15 @@ namespace mtconnect::sink::rest_sink { return true; } + /// @brief check if the routing's path pattern matches a given path (ignoring verb) + /// @param[in] path the request path to test + /// @return `true` if the path matches this routing's pattern + bool matchesPath(const std::string &path) const + { + std::smatch m; + return std::regex_match(path, m, m_pattern); + } + /// @brief check if this is related to a swagger API /// @returns `true` if related to swagger auto isSwagger() const { return m_swagger; } @@ -304,6 +316,10 @@ namespace mtconnect::sink::rest_sink { /// @brief Get the routing `verb` const auto &getVerb() const { return m_verb; } + /// @brief Check if the route is a catch-all (every path segment is a parameter) + /// @returns `true` if all path segments are parameters (e.g. `/{device}`) + auto isCatchAll() const { return m_catchAll; } + /// @brief Get the optional command associated with the routing /// @returns optional routing const auto &getCommand() const { return m_command; } @@ -319,21 +335,60 @@ namespace mtconnect::sink::rest_sink { protected: void pathParameters(std::string s) { - std::regex reg("\\{([^}]+)\\}"); - std::smatch match; std::stringstream pat; - while (regex_search(s, match, reg)) + using namespace boost::algorithm; + using SplitList = std::list>; + + SplitList parts; + auto pos = s.find_first_not_of('/'); + if (pos != std::string::npos) { - pat << match.prefix() << "([^/]+)"; - m_pathParameters.emplace_back(match[1]); - s = match.suffix().str(); + auto range = boost::make_iterator_range(s.begin() + pos, s.end()); + split(parts, range, [](char c) { return c == '/'; }); + } + + bool hasLiteral = false; + for (auto &p : parts) + { + auto start = p.begin(); + auto end = p.end(); + + auto openBrace = std::find(start, end, '{'); + decltype(openBrace) closeBrace {end}; + if (openBrace != end && std::distance(openBrace, end) > 2) + closeBrace = std::find(openBrace + 1, end, '}'); + + pat << "/"; + if (openBrace != end && closeBrace != end) + { + if (openBrace > start) + { + pat << std::string_view(start, openBrace); + hasLiteral = true; + } + std::string_view param(openBrace + 1, closeBrace); + pat << "([^/]+)"; + if (closeBrace + 1 < end) + { + pat << std::string_view(closeBrace + 1, end); + hasLiteral = true; + } + m_pathParameters.emplace_back(param); + } + else + { + pat << std::string_view(start, end); + hasLiteral = true; + } } - pat << s; pat << "/?"; m_patternText = pat.str(); m_pattern = std::regex(m_patternText); + + // A route is catch-all if it has parameters but no literal path segments + m_catchAll = !m_pathParameters.empty() && !hasLiteral; } void queryParameters(std::string s) @@ -513,5 +568,6 @@ namespace mtconnect::sink::rest_sink { std::optional m_description; bool m_swagger = false; + bool m_catchAll = false; }; } // namespace mtconnect::sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/server.cpp b/src/mtconnect/sink/rest_sink/server.cpp index b05804866..4cb1b8d2f 100644 --- a/src/mtconnect/sink/rest_sink/server.cpp +++ b/src/mtconnect/sink/rest_sink/server.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -26,6 +27,7 @@ #include #include #include +#include #include #include @@ -413,4 +415,44 @@ namespace mtconnect::sink::rest_sink { // addRouting({boost::beast::http::verb::get, "/swagger.yaml", handler, true}); } + bool Server::handleOptionsRequest(SessionPtr session, const RequestPtr request) + { + using namespace boost; + using namespace adaptors; + set specificVerbs; + set catchAllVerbs; + for (const auto &r : m_routings) + { + if (!r.isSwagger() && r.matchesPath(request->m_path)) + { + if (r.isCatchAll()) + catchAllVerbs.insert(r.getVerb()); + else + specificVerbs.insert(r.getVerb()); + } + } + + // If any specific route matched, use only those; otherwise fall back to catch-alls + auto &verbs = specificVerbs.empty() ? catchAllVerbs : specificVerbs; + + // OPTIONS is always allowed + verbs.insert(http::verb::options); + + // Build the Allow / Access-Control-Allow-Methods header value + string methods = algorithm::join( + verbs | transformed([](http::verb v) { return string(http::to_string(v)); }), ", "); + + auto response = std::make_unique(status::no_content, "", "text/plain"); + response->m_close = false; + response->m_fields.emplace_back("Allow", methods); + response->m_fields.emplace_back("Access-Control-Allow-Methods", methods); + response->m_fields.emplace_back("Access-Control-Allow-Headers", + "Content-Type, Accept, Accept-Encoding"); + response->m_fields.emplace_back("Access-Control-Max-Age", "86400"); + + session->writeResponse(std::move(response)); + + return true; + } + } // namespace mtconnect::sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/server.hpp b/src/mtconnect/sink/rest_sink/server.hpp index 2fdd8010e..73377b428 100644 --- a/src/mtconnect/sink/rest_sink/server.hpp +++ b/src/mtconnect/sink/rest_sink/server.hpp @@ -173,6 +173,10 @@ namespace mtconnect::sink::rest_sink { else message = "Command failed: " + *request->m_command; } + else if (request->m_verb == boost::beast::http::verb::options) + { + success = handleOptionsRequest(session, request); + } else { for (auto &r : m_routings) @@ -298,6 +302,16 @@ namespace mtconnect::sink::rest_sink { const void renderSwaggerResponse(T &format); /// @} + /// @name CORS Support + /// @{ + /// + /// @brief Handle OPTIONS request for CORS preflight requests + /// @param[in] session the client session + /// @param[in] request the incoming request + /// @return `true` if the request was handled, otherwise `false` and a 404 will be returned + bool handleOptionsRequest(SessionPtr session, const RequestPtr request); + /// @} + protected: boost::asio::io_context &m_context; diff --git a/src/mtconnect/sink/rest_sink/session_impl.cpp b/src/mtconnect/sink/rest_sink/session_impl.cpp index e241d5cc0..f188dd4db 100644 --- a/src/mtconnect/sink/rest_sink/session_impl.cpp +++ b/src/mtconnect/sink/rest_sink/session_impl.cpp @@ -199,8 +199,9 @@ namespace mtconnect::sink::rest_sink { auto &msg = m_parser->get(); const auto &remote = m_remote; - // Check for put, post, or delete - if (msg.method() != http::verb::get) + // Check for put, post, or delete (allow OPTIONS for CORS preflight) + if (msg.method() == http::verb::put || msg.method() == http::verb::post || + msg.method() == http::verb::delete_) { if (!m_allowPuts) { @@ -376,6 +377,10 @@ namespace mtconnect::sink::rest_sink { { res->set(http::field::location, *response.m_location); } + for (const auto &f : response.m_fields) + { + res->set(f.first, f.second); + } } template diff --git a/src/mtconnect/source/adapter/agent_adapter/agent_adapter.cpp b/src/mtconnect/source/adapter/agent_adapter/agent_adapter.cpp index 14fa7124e..4082300e8 100644 --- a/src/mtconnect/source/adapter/agent_adapter/agent_adapter.cpp +++ b/src/mtconnect/source/adapter/agent_adapter/agent_adapter.cpp @@ -137,6 +137,7 @@ namespace mtconnect::source::adapter::agent_adapter { m_name = m_url.getUrlText(m_sourceDevice); m_identity = CreateIdentityHash(m_name); + m_host = m_url.getHost(); m_options.insert_or_assign(configuration::AdapterIdentity, m_identity); m_feedbackId = "XmlTransformFeedback:" + m_identity; @@ -180,18 +181,18 @@ namespace mtconnect::source::adapter::agent_adapter { return false; } - m_session->m_handler = m_handler.get(); - m_session->m_identity = m_identity; - m_session->m_closeConnectionAfterResponse = m_closeConnectionAfterResponse; - m_session->m_updateAssets = [this]() { updateAssets(); }; + m_session->setHandler(m_handler.get()); + m_session->setIdentity(m_identity); + m_session->setCloseConnectionAfterResponse(m_closeConnectionAfterResponse); + m_session->setUpdateAssets([this]() { updateAssets(); }); - m_assetSession->m_handler = m_handler.get(); - m_assetSession->m_identity = m_identity; - m_assetSession->m_closeConnectionAfterResponse = m_closeConnectionAfterResponse; + m_assetSession->setHandler(m_handler.get()); + m_assetSession->setIdentity(m_identity); + m_assetSession->setCloseConnectionAfterResponse(m_closeConnectionAfterResponse); using namespace std::placeholders; - m_assetSession->m_failed = std::bind(&AgentAdapter::assetsFailed, this, _1); - m_session->m_failed = std::bind(&AgentAdapter::streamsFailed, this, _1); + m_assetSession->setFailed(std::bind(&AgentAdapter::assetsFailed, this, _1)); + m_session->setFailed(std::bind(&AgentAdapter::streamsFailed, this, _1)); run(); @@ -227,15 +228,16 @@ namespace mtconnect::source::adapter::agent_adapter { } m_reconnectTimer.expires_after(m_reconnectInterval); - m_reconnectTimer.async_wait(asio::bind_executor(m_strand, [this](boost::system::error_code ec) { - if (!ec) - { - if (canRecover() && m_streamRequest) - m_session->makeRequest(*m_streamRequest); - else - run(); - } - })); + m_reconnectTimer.async_wait(asio::bind_executor( + m_strand, [weak = std::weak_ptr(getptr())](boost::system::error_code ec) { + if (auto self = weak.lock(); self && !ec) + { + if (self->canRecover() && self->m_streamRequest) + self->m_session->makeRequest(*self->m_streamRequest); + else + self->run(); + } + })); } void AgentAdapter::recoverAssetRequest() @@ -243,11 +245,11 @@ namespace mtconnect::source::adapter::agent_adapter { if (m_assetRequest) { m_assetRetryTimer.expires_after(m_reconnectInterval); - m_assetRetryTimer.async_wait( - asio::bind_executor(m_strand, [this](boost::system::error_code ec) { - if (!ec && m_assetRequest) + m_assetRetryTimer.async_wait(asio::bind_executor( + m_strand, [weak = std::weak_ptr(getptr())](boost::system::error_code ec) { + if (auto self = weak.lock(); self && !ec && self->m_assetRequest) { - m_assetSession->makeRequest(*m_assetRequest); + self->m_assetSession->makeRequest(*self->m_assetRequest); } })); } @@ -406,11 +408,11 @@ namespace mtconnect::source::adapter::agent_adapter { {"count", lexical_cast(m_count)}}); m_streamRequest.emplace(m_sourceDevice, "sample", query, false, [this]() { m_pollingTimer.expires_after(m_pollingInterval); - m_pollingTimer.async_wait( - asio::bind_executor(m_strand, [this](boost::system::error_code ec) { - if (!ec && m_streamRequest) + m_pollingTimer.async_wait(asio::bind_executor( + m_strand, [weak = std::weak_ptr(getptr())](boost::system::error_code ec) { + if (auto self = weak.lock(); self && !ec && self->m_streamRequest) { - sample(); + self->sample(); } })); return true; diff --git a/src/mtconnect/source/adapter/agent_adapter/http_session.hpp b/src/mtconnect/source/adapter/agent_adapter/http_session.hpp index 08f25669c..0e23ac461 100644 --- a/src/mtconnect/source/adapter/agent_adapter/http_session.hpp +++ b/src/mtconnect/source/adapter/agent_adapter/http_session.hpp @@ -45,9 +45,9 @@ namespace mtconnect::source::adapter::agent_adapter { /// @brief Get a shared pointer to this /// @return shared pointer to this - shared_ptr getptr() + std::shared_ptr getptr() { - return static_pointer_cast(shared_from_this()); + return std::static_pointer_cast(shared_from_this()); } /// @brief Get the boost asio tcp stream @@ -75,7 +75,7 @@ namespace mtconnect::source::adapter::agent_adapter { if (!m_request) { m_stream.close(); - LOG(error) << "Connected and no reqiest"; + LOG(error) << "Connected and no request"; return failed(source::make_error_code(ErrorCode::RETRY_REQUEST), "connect"); } diff --git a/src/mtconnect/source/adapter/agent_adapter/https_session.hpp b/src/mtconnect/source/adapter/agent_adapter/https_session.hpp index 698821ba4..243c5d3fc 100644 --- a/src/mtconnect/source/adapter/agent_adapter/https_session.hpp +++ b/src/mtconnect/source/adapter/agent_adapter/https_session.hpp @@ -56,7 +56,7 @@ namespace mtconnect::source::adapter::agent_adapter { /// @return const lowest protocol layer const auto &lowestLayer() const { return beast::get_lowest_layer(m_stream); } - shared_ptr getptr() + std::shared_ptr getptr() { return std::static_pointer_cast(shared_from_this()); } @@ -88,7 +88,7 @@ namespace mtconnect::source::adapter::agent_adapter { if (!m_request) { lowestLayer().close(); - LOG(error) << "Connected and no reqiest"; + LOG(error) << "Connected and no request"; return failed(source::make_error_code(ErrorCode::RETRY_REQUEST), "connect"); } @@ -160,6 +160,6 @@ namespace mtconnect::source::adapter::agent_adapter { protected: beast::ssl_stream m_stream; - optional m_sslContext; + std::optional m_sslContext; }; } // namespace mtconnect::source::adapter::agent_adapter diff --git a/src/mtconnect/source/adapter/agent_adapter/session.hpp b/src/mtconnect/source/adapter/agent_adapter/session.hpp index 6cd87de9e..abb69db4b 100644 --- a/src/mtconnect/source/adapter/agent_adapter/session.hpp +++ b/src/mtconnect/source/adapter/agent_adapter/session.hpp @@ -101,13 +101,24 @@ namespace mtconnect::source::adapter::agent_adapter { virtual bool makeRequest(const Request &request) = 0; ///@} - Handler *m_handler = nullptr; - std::string m_identity; - Failure m_failed; - UpdateAssets m_updateAssets; - bool m_closeConnectionAfterResponse = false; - std::chrono::milliseconds m_timeout = std::chrono::milliseconds(30000); - bool m_closeOnRead = false; + /// @name Setters for session configuration + ///@{ + void setHandler(Handler *handler) { m_handler = handler; } + void setIdentity(const std::string &identity) { m_identity = identity; } + void setFailed(Failure failed) { m_failed = std::move(failed); } + void setUpdateAssets(UpdateAssets updateAssets) { m_updateAssets = std::move(updateAssets); } + void setCloseConnectionAfterResponse(bool close) { m_closeConnectionAfterResponse = close; } + void setTimeout(std::chrono::milliseconds timeout) { m_timeout = timeout; } + ///@} + + protected: + Handler *m_handler = nullptr; ///< Pipeline handler for processing data + std::string m_identity; ///< Unique identity hash for this session + Failure m_failed; ///< Callback invoked on connection failure + UpdateAssets m_updateAssets; ///< Callback to trigger asset updates + bool m_closeConnectionAfterResponse = false; ///< Close connection after each response + std::chrono::milliseconds m_timeout = std::chrono::milliseconds(30000); ///< I/O timeout + bool m_closeOnRead = false; ///< Close after read (HTTP 1.0 or Connection: close) }; } // namespace mtconnect::source::adapter::agent_adapter diff --git a/src/mtconnect/source/adapter/agent_adapter/session_impl.hpp b/src/mtconnect/source/adapter/agent_adapter/session_impl.hpp index c0811e129..ff4d02671 100644 --- a/src/mtconnect/source/adapter/agent_adapter/session_impl.hpp +++ b/src/mtconnect/source/adapter/agent_adapter/session_impl.hpp @@ -32,7 +32,6 @@ #include "session.hpp" namespace mtconnect::source::adapter::agent_adapter { - using namespace std; namespace asio = boost::asio; namespace beast = boost::beast; namespace http = boost::beast::http; @@ -79,7 +78,7 @@ namespace mtconnect::source::adapter::agent_adapter { { derived().lowestLayer().socket().close(); - LOG(error) << "Agent Adapter Connection Failed: " << m_url.getUrlText(nullopt); + LOG(error) << "Agent Adapter Connection Failed: " << m_url.getUrlText(std::nullopt); if (m_request) LOG(error) << "Agent Adapter Target: " << m_request->getTarget(m_url); LOG(error) << "Agent Adapter " << what << ": " << ec.message() << "\n"; @@ -177,9 +176,9 @@ namespace mtconnect::source::adapter::agent_adapter { beast::error_code ec; onResolve(ec, *m_resolution); } - else if (holds_alternative(m_url.m_host)) + else if (std::holds_alternative(m_url.m_host)) { - asio::ip::tcp::endpoint ep(get(m_url.m_host), m_url.getPort()); + asio::ip::tcp::endpoint ep(std::get(m_url.m_host), m_url.getPort()); // Create the results type and call on resolve directly. using results_type = tcp::resolver::results_type; @@ -194,7 +193,7 @@ namespace mtconnect::source::adapter::agent_adapter { // Do an async resolution of the address. m_resolver.async_resolve( - get(m_url.m_host), m_url.getService(), + std::get(m_url.m_host), m_url.getService(), asio::bind_executor( m_strand, beast::bind_front_handler(&SessionImpl::onResolve, derived().getptr()))); } @@ -256,7 +255,7 @@ namespace mtconnect::source::adapter::agent_adapter { derived().lowestLayer().expires_after(m_timeout); - LOG(debug) << "Agent adapter making request: " << m_url.getUrlText(nullopt) << " target " + LOG(debug) << "Agent adapter making request: " << m_url.getUrlText(std::nullopt) << " target " << m_request->getTarget(m_url); http::async_write(derived().stream(), *m_req, @@ -311,8 +310,6 @@ namespace mtconnect::source::adapter::agent_adapter { { return failed(source::make_error_code(ErrorCode::RETRY_REQUEST), "header"); } - - return; } if (!m_request) @@ -399,23 +396,28 @@ namespace mtconnect::source::adapter::agent_adapter { /// @brief Find the x-multipart-replace MIME boundary /// @return the boundary string - inline string findBoundary() + inline std::string findBoundary() { auto f = m_headerParser->get().find(http::field::content_type); if (f != m_headerParser->get().end()) { - m_contentType = string(f->value()); + m_contentType = std::string(f->value()); auto i = m_contentType.find(';'); - if (i != string::npos) + if (i != std::string::npos) { auto b = m_contentType.substr(i + 1); m_contentType = m_contentType.substr(0, i); + boost::algorithm::trim(m_contentType); auto p = b.find("="); - if (p != string::npos) + if (p != std::string::npos) { - if (b.substr(0, p) == "boundary") + auto key = b.substr(0, p); + auto value = b.substr(p + 1); + boost::algorithm::trim(key); + boost::algorithm::trim(value); + if (key == "boundary") { - return "--"s + b.substr(p + 1); + return "--" + value; } } } @@ -431,12 +433,6 @@ namespace mtconnect::source::adapter::agent_adapter { { m_chunkHeaderHandler = [this](std::uint64_t size, boost::string_view extensions, boost::system::error_code &ec) { -#if 0 - http::chunk_extensions ce; - ce.parse(extensions, ec); - for (auto &c : ce) - cout << "Ext: " << c.first << ": " << c.second << endl; -#endif derived().lowestLayer().expires_after(m_timeout); if (ec) @@ -475,7 +471,7 @@ namespace mtconnect::source::adapter::agent_adapter { } auto ep = view.find("\r\n\r\n", bp); - if (bp == boost::string_view::npos) + if (ep == boost::string_view::npos) { LOG(warning) << "Cannot find the header separator"; derived().lowestLayer().close(); @@ -524,7 +520,7 @@ namespace mtconnect::source::adapter::agent_adapter { void createChunkBodyHandler() { m_chunkHandler = [this](std::uint64_t remain, boost::string_view body, - boost::system::error_code &ev) -> unsigned long { + boost::system::error_code &ev) -> std::size_t { if (!m_request) { derived().lowestLayer().close(); @@ -554,11 +550,11 @@ namespace mtconnect::source::adapter::agent_adapter { if (len >= m_chunkLength) { auto start = static_cast(m_chunk.data().data()); - string_view sbuf(start, m_chunkLength); + boost::string_view sbuf(start, m_chunkLength); LOG(trace) << "Received Chunk: --------\n" << sbuf << "\n-------------"; - processData(string(sbuf)); + processData(std::string(sbuf)); m_chunk.consume(m_chunkLength); m_hasHeader = false; @@ -607,7 +603,7 @@ namespace mtconnect::source::adapter::agent_adapter { asio::io_context::strand m_strand; url::Url m_url; - std::function + std::function m_chunkHandler; std::function m_chunkHeaderHandler; diff --git a/src/mtconnect/source/adapter/shdr/connector.cpp b/src/mtconnect/source/adapter/shdr/connector.cpp index 551b79a5f..be774fe60 100644 --- a/src/mtconnect/source/adapter/shdr/connector.cpp +++ b/src/mtconnect/source/adapter/shdr/connector.cpp @@ -98,10 +98,10 @@ namespace mtconnect::source::adapter::shdr { if (ec) { - LOG(error) << "Cannot resolve address: " << m_server << ":" << m_port; - LOG(error) << ec.category().message(ec.value()) << ": " << ec.message(); - LOG(error) << "Will retry resolution of " << m_server << " in " << m_reconnectInterval.count() - << " milliseconds"; + LOG(warning) << "Cannot resolve address: " << m_server << ":" << m_port; + LOG(warning) << ec.message(); + LOG(warning) << "Will retry resolution of " << m_server << " in " + << m_reconnectInterval.count() << " milliseconds"; m_timer.expires_after(m_reconnectInterval); m_timer.async_wait([this](boost::system::error_code ec) { @@ -136,8 +136,9 @@ namespace mtconnect::source::adapter::shdr { return true; } - /// @brief Attempt to reconnect after a delay. If the server is a hostname, re-resolve it to get the current IP - /// address in case it has changed. If the server is a static IP address, just reconnect. + /// @brief Attempt to reconnect after a delay. If the server is a hostname, re-resolve it to get + /// the current IP address in case it has changed. If the server is a static IP address, just + /// reconnect. inline void Connector::asyncTryConnect() { NAMED_SCOPE("Connector::asyncTryConnect"); @@ -203,7 +204,7 @@ namespace mtconnect::source::adapter::shdr { auto remote = m_socket.remote_endpoint(rec); if (rec) { - LOG(error) << "Failed to get remote endpoint: " << rec.message(); + LOG(warning) << "Failed to get remote endpoint: " << rec.message(); } else { @@ -233,7 +234,7 @@ namespace mtconnect::source::adapter::shdr { if (ec) { - LOG(error) << ec.category().message(ec.value()) << ": " << ec.message(); + LOG(error) << ec.message(); reconnect(); } else @@ -271,7 +272,7 @@ namespace mtconnect::source::adapter::shdr { if (ec) { - LOG(error) << ec.category().message(ec.value()) << ": " << ec.message(); + LOG(error) << ec.message(); reconnect(); } } @@ -292,15 +293,14 @@ namespace mtconnect::source::adapter::shdr { m_receiveTimeout.async_wait([this](sys::error_code ec) { if (!ec) { - LOG(error) << "(Port:" << m_localPort << ")" - << " connect: Did not receive data for over: " << m_receiveTimeLimit.count() - << " ms"; + LOG(warning) << "(Port:" << m_localPort << ")" + << " connect: Did not receive data for over: " << m_receiveTimeLimit.count() + << " ms"; asio::dispatch(m_strand, boost::bind(&Connector::reconnect, this)); } else if (ec != boost::asio::error::operation_aborted) { - LOG(error) << "Receive timeout: " << ec.category().message(ec.value()) << ": " - << ec.message(); + LOG(error) << "Receive timeout: " << ec.message(); } }); } @@ -413,7 +413,7 @@ namespace mtconnect::source::adapter::shdr { } else if (ec != boost::asio::error::operation_aborted) { - LOG(error) << "heartbeat: " << ec.category().message(ec.value()) << ": " << ec.message(); + LOG(error) << "heartbeat: " << ec.message(); } } diff --git a/test_package/config_test.cpp b/test_package/config_test.cpp index fde6e2f1b..a7322a1fa 100644 --- a/test_package/config_test.cpp +++ b/test_package/config_test.cpp @@ -23,6 +23,7 @@ // Keep this comment to keep gtest.h above. (clang-format off/on is not working here!) #include +#include #include #include @@ -779,13 +780,17 @@ MaxCachedFileSize = 2g TEST_F(ConfigTest, log_output_should_set_archive_file_pattern) { + auto root {createTempDirectory("log_1")}; + m_config->setConfigPath(root); m_config->setDebug(false); - string str(R"( + string str = "Devices = " TEST_RESOURCE_DIR + "/samples/min_config.xml" + R"( logger_config { output = file agent.log } -)"); +)"; m_config->loadConfig(str); @@ -794,18 +799,22 @@ logger_config { EXPECT_EQ("agent_%Y-%m-%d_%H-%M-%S_%N.log", m_config->getLogArchivePattern().filename()); EXPECT_EQ("agent.log", m_config->getLogFileName().filename()); - EXPECT_PATH_EQ(TEST_BIN_ROOT_DIR, m_config->getLogDirectory()); + EXPECT_PATH_EQ(root, m_config->getLogDirectory()); } TEST_F(ConfigTest, log_output_should_configure_file_name) { + auto root {createTempDirectory("log_2")}; + m_config->setConfigPath(root); m_config->setDebug(false); - string str(R"( + string str = "Devices = " TEST_RESOURCE_DIR + "/samples/min_config.xml" + R"( logger_config { output = file logging.log logging_%N.log } -)"); +)"; m_config->loadConfig(str); @@ -814,19 +823,23 @@ logger_config { EXPECT_EQ("logging_%N.log", m_config->getLogArchivePattern().filename()); EXPECT_EQ("logging.log", m_config->getLogFileName().filename()); - EXPECT_PATH_EQ(TEST_BIN_ROOT_DIR, m_config->getLogDirectory()); + EXPECT_PATH_EQ(root, m_config->getLogDirectory()); } TEST_F(ConfigTest, log_should_configure_file_name) { + auto root {createTempDirectory("log_3")}; + m_config->setConfigPath(root); m_config->setDebug(false); - string str(R"( + string str = "Devices = " TEST_RESOURCE_DIR + "/samples/min_config.xml" + R"( logger_config { file_name = logging.log archive_pattern = logging_%N.log } -)"); +)"; m_config->loadConfig(str); @@ -835,26 +848,30 @@ logger_config { EXPECT_EQ("logging_%N.log", m_config->getLogArchivePattern().filename()); EXPECT_EQ("logging.log", m_config->getLogFileName().filename()); - EXPECT_PATH_EQ(TEST_BIN_ROOT_DIR, m_config->getLogDirectory()); + EXPECT_PATH_EQ(root, m_config->getLogDirectory()); } TEST_F(ConfigTest, log_should_specify_relative_directory) { + auto root {createTempDirectory("log_3")}; + m_config->setConfigPath(root); m_config->setDebug(false); - string str(R"( + string str = "Devices = " TEST_RESOURCE_DIR + "/samples/min_config.xml" + R"( logger_config { file_name = logging.log archive_pattern = logs/logging_%N.log } -)"); +)"; m_config->loadConfig(str); auto sink = m_config->getLoggerSink(); ASSERT_TRUE(sink); - fs::path path {std::filesystem::canonical(TEST_BIN_ROOT_DIR) / "logs"}; + fs::path path {root / "logs"}; EXPECT_PATH_EQ(path / "logging_%N.log", m_config->getLogArchivePattern()); EXPECT_PATH_EQ(path / "logging.log", m_config->getLogFileName()); @@ -863,38 +880,47 @@ logger_config { TEST_F(ConfigTest, log_should_specify_relative_directory_with_active_in_parent) { + auto root {createTempDirectory("log_4")}; + m_config->setConfigPath(root); m_config->setDebug(false); - string str(R"( + string str = "Devices = " TEST_RESOURCE_DIR + "/samples/min_config.xml" + R"( logger_config { file_name = ./logging.log archive_pattern = logs/logging_%N.log } -)"); +)"; m_config->loadConfig(str); auto sink = m_config->getLoggerSink(); ASSERT_TRUE(sink); - fs::path path {std::filesystem::canonical(TEST_BIN_ROOT_DIR)}; + fs::path path {std::filesystem::canonical(root)}; EXPECT_PATH_EQ(path / "logs" / "logging_%N.log", m_config->getLogArchivePattern()); EXPECT_PATH_EQ(path / "logging.log", m_config->getLogFileName()); - EXPECT_PATH_EQ(path / "logs", m_config->getLogDirectory()); + EXPECT_PATH_EQ(path / "logs", m_config->getArchiveLogDirectory()); } TEST_F(ConfigTest, log_should_specify_max_file_and_rotation_size) { - m_config->setDebug(false); using namespace boost::log::trivial; - string str(R"( + auto root {createTempDirectory("log_5")}; + m_config->setConfigPath(root); + m_config->setDebug(false); + + string str = "Devices = " TEST_RESOURCE_DIR + "/samples/min_config.xml" + R"( logger_config { max_size = 1gb - rotation_size = 20gb + max_archive_size = 20gb } -)"); +)"; m_config->loadConfig(str); @@ -902,21 +928,25 @@ logger_config { ASSERT_TRUE(sink); EXPECT_EQ(severity_level::info, m_config->getLogLevel()); - EXPECT_EQ(1ll * 1024 * 1024 * 1024, m_config->getMaxLogFileSize()); - EXPECT_EQ(20ll * 1024 * 1024 * 1024, m_config->getLogRotationSize()); + EXPECT_EQ(1ll * 1024 * 1024 * 1024, m_config->getLogRotationSize()); + EXPECT_EQ(20ll * 1024 * 1024 * 1024, m_config->getMaxLogArchiveSize()); } TEST_F(ConfigTest, log_should_configure_logging_level) { - m_config->setDebug(false); - using namespace boost::log::trivial; - string str(R"( + auto root {createTempDirectory("log_6")}; + m_config->setConfigPath(root); + m_config->setDebug(false); + + string str = "Devices = " TEST_RESOURCE_DIR + "/samples/min_config.xml" + R"( logger_config { level = fatal } -)"); +)"; m_config->loadConfig(str); @@ -968,6 +998,53 @@ logger_config { EXPECT_EQ(severity_level::fatal, m_config->getLogLevel()); } + TEST_F(ConfigTest, log_should_rotate_log_file_when_it_reaches_limit) + { + auto root {createTempDirectory("log_7")}; + m_config->setConfigPath(root); + m_config->setDebug(false); + + string str = "Devices = " TEST_RESOURCE_DIR + "/samples/min_config.xml" + R"( +logger_config { + file_name = ./logging.log + archive_pattern = logs/logging_%N.log + # Make if very small + max_size = 1k +} +)"; + + m_config->loadConfig(str); + + auto sink = m_config->getLoggerSink(); + ASSERT_TRUE(sink); + + fs::path path {std::filesystem::canonical(root)}; + + EXPECT_PATH_EQ(path / "logs" / "logging_%N.log", m_config->getLogArchivePattern()); + EXPECT_PATH_EQ(path / "logging.log", m_config->getLogFileName()); + EXPECT_PATH_EQ(path / "logs", m_config->getArchiveLogDirectory()); + + // Write some data to the log file to trigger rotation + for (auto _ : boost::irange(11)) + LOG(info) << "This is a long 100 byte test log message to trigger rotation of file with some " + "common text included."; + + auto logging = path / "logging.log"; + EXPECT_TRUE(fs::exists(logging)) << "Expected log file to exist: " << logging; + EXPECT_TRUE(fs::file_size(logging) < 1024) + << "Expected log file to be less than 1KB: " << logging; + + for (auto i : boost::irange(2)) + { + fs::path rotated = path / "logs" / ("logging_" + std::to_string(i) + ".log"); + EXPECT_TRUE(fs::exists(rotated)) << "Expected log file to be rotated: " << rotated; + EXPECT_TRUE(fs::file_size(rotated) < 1024) + << "Expected rotated log file to be less than 1KB: " << rotated; + } + } + TEST_F(ConfigTest, should_reload_device_xml_file) { auto root {createTempDirectory("1")}; diff --git a/test_package/data_item_mapping_test.cpp b/test_package/data_item_mapping_test.cpp index 380677dcc..b06d48cb3 100644 --- a/test_package/data_item_mapping_test.cpp +++ b/test_package/data_item_mapping_test.cpp @@ -106,8 +106,8 @@ class DataItemMappingTest : public testing::Test std::map m_dataItems; }; -inline DataSetEntry operator"" _E(const char *c, std::size_t) { return DataSetEntry(c); } -inline TableCell operator"" _C(const char *c, std::size_t) { return TableCell(c); } +inline DataSetEntry operator""_E(const char *c, std::size_t) { return DataSetEntry(c); } +inline TableCell operator""_C(const char *c, std::size_t) { return TableCell(c); } TEST_F(DataItemMappingTest, should_map_simple_sample) { diff --git a/test_package/data_set_test.cpp b/test_package/data_set_test.cpp index 6302ea3b3..48464d87a 100644 --- a/test_package/data_set_test.cpp +++ b/test_package/data_set_test.cpp @@ -77,7 +77,7 @@ using namespace std::literals; using namespace chrono_literals; using namespace date::literals; -inline DataSetEntry operator"" _E(const char *c, std::size_t) { return DataSetEntry(c); } +inline DataSetEntry operator""_E(const char *c, std::size_t) { return DataSetEntry(c); } TEST_F(DataSetTest, DataItem) { diff --git a/test_package/entity_test.cpp b/test_package/entity_test.cpp index 2cf031928..68f4d926d 100644 --- a/test_package/entity_test.cpp +++ b/test_package/entity_test.cpp @@ -41,7 +41,7 @@ int main(int argc, char *argv[]) return RUN_ALL_TESTS(); } -static inline int64_t operator"" _i64(unsigned long long int i) { return int64_t(i); } +static inline int64_t operator""_i64(unsigned long long int i) { return int64_t(i); } class EntityTest : public testing::Test { diff --git a/test_package/http_server_test.cpp b/test_package/http_server_test.cpp index 81c0a1c56..86f733045 100644 --- a/test_package/http_server_test.cpp +++ b/test_package/http_server_test.cpp @@ -275,7 +275,7 @@ class Client map m_fields; string m_contentType; - std::function + std::function m_chunkHandler; std::function m_headerHandler; @@ -683,6 +683,301 @@ TEST_F(HttpServerTest, additional_header_fields) ASSERT_EQ("https://foo.example", f2->second); } +TEST_F(HttpServerTest, options_returns_allowed_methods_for_get_only_path) +{ + auto handler = [&](SessionPtr session, RequestPtr request) -> bool { + ResponsePtr resp = make_unique(status::ok, "Done"); + session->writeResponse(std::move(resp)); + return true; + }; + + m_server->addRouting({http::verb::get, "/probe", handler}); + + start(); + startClient(); + + m_client->spawnRequest(http::verb::options, "/probe"); + ASSERT_TRUE(m_client->m_done); + EXPECT_EQ(int(http::status::no_content), m_client->m_status); + + auto allow = m_client->m_fields.find("Allow"); + ASSERT_NE(m_client->m_fields.end(), allow); + EXPECT_NE(string::npos, allow->second.find("GET")); + EXPECT_NE(string::npos, allow->second.find("OPTIONS")); + EXPECT_EQ(string::npos, allow->second.find("PUT")); + EXPECT_EQ(string::npos, allow->second.find("POST")); + EXPECT_EQ(string::npos, allow->second.find("DELETE")); + + auto acam = m_client->m_fields.find("Access-Control-Allow-Methods"); + ASSERT_NE(m_client->m_fields.end(), acam); + EXPECT_EQ(allow->second, acam->second); + + auto acah = m_client->m_fields.find("Access-Control-Allow-Headers"); + ASSERT_NE(m_client->m_fields.end(), acah); + + auto acma = m_client->m_fields.find("Access-Control-Max-Age"); + ASSERT_NE(m_client->m_fields.end(), acma); + EXPECT_EQ("86400", acma->second); +} + +TEST_F(HttpServerTest, options_returns_get_put_and_delete_when_registered) +{ + auto getHandler = [&](SessionPtr session, RequestPtr request) -> bool { + ResponsePtr resp = make_unique(status::ok, "Done"); + session->writeResponse(std::move(resp)); + return true; + }; + auto putHandler = [&](SessionPtr session, RequestPtr request) -> bool { + ResponsePtr resp = make_unique(status::ok, "Put ok"); + session->writeResponse(std::move(resp)); + return true; + }; + auto deleteHandler = [&](SessionPtr session, RequestPtr request) -> bool { + ResponsePtr resp = make_unique(status::ok, "Deleted"); + session->writeResponse(std::move(resp)); + return true; + }; + + m_server->addRouting({http::verb::get, "/asset/{id}", getHandler}); + m_server->addRouting({http::verb::put, "/asset/{id}", putHandler}); + m_server->addRouting({http::verb::delete_, "/asset/{id}", deleteHandler}); + m_server->allowPuts(); + + start(); + startClient(); + + m_client->spawnRequest(http::verb::options, "/asset/123"); + ASSERT_TRUE(m_client->m_done); + EXPECT_EQ(int(http::status::no_content), m_client->m_status); + + auto allow = m_client->m_fields.find("Allow"); + ASSERT_NE(m_client->m_fields.end(), allow); + EXPECT_NE(string::npos, allow->second.find("GET")); + EXPECT_NE(string::npos, allow->second.find("OPTIONS")); + EXPECT_NE(string::npos, allow->second.find("PUT")); + EXPECT_EQ(string::npos, allow->second.find("POST")); + EXPECT_NE(string::npos, allow->second.find("DELETE")); +} + +TEST_F(HttpServerTest, options_returns_get_when_a_specific_and_wildcard_route_are_given) +{ + auto getHandler = [&](SessionPtr session, RequestPtr request) -> bool { + ResponsePtr resp = make_unique(status::ok, "Done"); + session->writeResponse(std::move(resp)); + return true; + }; + auto putHandler = [&](SessionPtr session, RequestPtr request) -> bool { + ResponsePtr resp = make_unique(status::ok, "Put ok"); + session->writeResponse(std::move(resp)); + return true; + }; + auto deleteHandler = [&](SessionPtr session, RequestPtr request) -> bool { + ResponsePtr resp = make_unique(status::ok, "Deleted"); + session->writeResponse(std::move(resp)); + return true; + }; + + m_server->addRouting({http::verb::get, "/current", getHandler}); + m_server->addRouting({http::verb::put, "/{device}?timestamp={timestamp}", putHandler}); + m_server->addRouting({http::verb::delete_, "/{device}?timestamp={timestamp}", deleteHandler}); + m_server->allowPuts(); + + start(); + startClient(); + + m_client->spawnRequest(http::verb::options, "/current"); + ASSERT_TRUE(m_client->m_done); + EXPECT_EQ(int(http::status::no_content), m_client->m_status); + + auto allow = m_client->m_fields.find("Allow"); + ASSERT_NE(m_client->m_fields.end(), allow); + EXPECT_NE(string::npos, allow->second.find("GET")); + EXPECT_NE(string::npos, allow->second.find("OPTIONS")); + EXPECT_EQ(string::npos, allow->second.find("PUT")); + EXPECT_EQ(string::npos, allow->second.find("POST")); + EXPECT_EQ(string::npos, allow->second.find("DELETE")); +} + +TEST_F(HttpServerTest, options_returns_get_when_complex_path_route_are_given) +{ + auto getHandler = [&](SessionPtr session, RequestPtr request) -> bool { + ResponsePtr resp = make_unique(status::ok, "Done"); + session->writeResponse(std::move(resp)); + return true; + }; + auto putHandler = [&](SessionPtr session, RequestPtr request) -> bool { + ResponsePtr resp = make_unique(status::ok, "Put ok"); + session->writeResponse(std::move(resp)); + return true; + }; + auto deleteHandler = [&](SessionPtr session, RequestPtr request) -> bool { + ResponsePtr resp = make_unique(status::ok, "Deleted"); + session->writeResponse(std::move(resp)); + return true; + }; + + m_server->addRouting({http::verb::get, "/{device}/current", getHandler}); + m_server->addRouting({http::verb::put, "/{device}/{command}?timestamp={timestamp}", putHandler}); + m_server->addRouting( + {http::verb::delete_, "/{device}/{command}?timestamp={timestamp}", deleteHandler}); + m_server->allowPuts(); + + start(); + startClient(); + + m_client->spawnRequest(http::verb::options, "/mydevice/current"); + ASSERT_TRUE(m_client->m_done); + EXPECT_EQ(int(http::status::no_content), m_client->m_status); + + auto allow = m_client->m_fields.find("Allow"); + ASSERT_NE(m_client->m_fields.end(), allow); + EXPECT_NE(string::npos, allow->second.find("GET")); + EXPECT_NE(string::npos, allow->second.find("OPTIONS")); + EXPECT_EQ(string::npos, allow->second.find("PUT")); + EXPECT_EQ(string::npos, allow->second.find("POST")); + EXPECT_EQ(string::npos, allow->second.find("DELETE")); +} + +TEST_F(HttpServerTest, options_allowed_even_when_puts_disabled) +{ + auto handler = [&](SessionPtr session, RequestPtr request) -> bool { + ResponsePtr resp = make_unique(status::ok, "Done"); + session->writeResponse(std::move(resp)); + return true; + }; + + m_server->addRouting({http::verb::get, "/probe", handler}); + // Note: puts are NOT enabled + + start(); + startClient(); + + m_client->spawnRequest(http::verb::options, "/probe"); + ASSERT_TRUE(m_client->m_done); + + // OPTIONS should succeed even though puts are disabled + EXPECT_EQ(int(http::status::no_content), m_client->m_status); + + auto allow = m_client->m_fields.find("Allow"); + ASSERT_NE(m_client->m_fields.end(), allow); + EXPECT_NE(string::npos, allow->second.find("GET")); + EXPECT_NE(string::npos, allow->second.find("OPTIONS")); + EXPECT_EQ(string::npos, allow->second.find("PUT")); + EXPECT_EQ(string::npos, allow->second.find("POST")); + EXPECT_EQ(string::npos, allow->second.find("DELETE")); +} + +TEST_F(HttpServerTest, options_includes_configured_cors_origin_header) +{ + m_server->setHttpHeaders({"Access-Control-Allow-Origin:*"}); + + auto handler = [&](SessionPtr session, RequestPtr request) -> bool { + ResponsePtr resp = make_unique(status::ok, "Done"); + session->writeResponse(std::move(resp)); + return true; + }; + + m_server->addRouting({http::verb::get, "/probe", handler}); + + start(); + startClient(); + + m_client->spawnRequest(http::verb::options, "/probe"); + ASSERT_TRUE(m_client->m_done); + EXPECT_EQ(int(http::status::no_content), m_client->m_status); + + // Access-Control-Allow-Origin comes from the configured HttpHeaders + auto acao = m_client->m_fields.find("Access-Control-Allow-Origin"); + ASSERT_NE(m_client->m_fields.end(), acao); + ASSERT_EQ("*", acao->second); + + // Access-Control-Allow-Methods comes from the OPTIONS handler + auto acam = m_client->m_fields.find("Access-Control-Allow-Methods"); + ASSERT_NE(m_client->m_fields.end(), acam); + EXPECT_NE(string::npos, acam->second.find("GET")); + EXPECT_NE(string::npos, acam->second.find("OPTIONS")); + EXPECT_EQ(string::npos, acam->second.find("PUT")); + EXPECT_EQ(string::npos, acam->second.find("POST")); + EXPECT_EQ(string::npos, acam->second.find("DELETE")); +} + +TEST_F(HttpServerTest, options_returns_correctly_for_path_with_parameter_value) +{ + auto handler = [&](SessionPtr session, RequestPtr request) -> bool { + ResponsePtr resp = make_unique(status::ok, "Done"); + session->writeResponse(std::move(resp)); + return true; + }; + + m_server->addRouting({boost::beast::http::verb::get, "/cancel/id={string}", handler}) + .document("MTConnect WebServices Cancel Stream", "Cancels a streaming sample request") + .command("cancel"); + + start(); + startClient(); + + m_client->spawnRequest(http::verb::options, "/cancel/id=12345"); + ASSERT_TRUE(m_client->m_done); + EXPECT_EQ(int(http::status::no_content), m_client->m_status); + + auto allow = m_client->m_fields.find("Allow"); + ASSERT_NE(m_client->m_fields.end(), allow); + EXPECT_NE(string::npos, allow->second.find("GET")); + EXPECT_NE(string::npos, allow->second.find("OPTIONS")); + EXPECT_EQ(string::npos, allow->second.find("PUT")); + EXPECT_EQ(string::npos, allow->second.find("POST")); + EXPECT_EQ(string::npos, allow->second.find("DELETE")); + + auto acam = m_client->m_fields.find("Access-Control-Allow-Methods"); + ASSERT_NE(m_client->m_fields.end(), acam); + EXPECT_EQ(allow->second, acam->second); + + auto acah = m_client->m_fields.find("Access-Control-Allow-Headers"); + ASSERT_NE(m_client->m_fields.end(), acah); + + auto acma = m_client->m_fields.find("Access-Control-Max-Age"); + ASSERT_NE(m_client->m_fields.end(), acma); + EXPECT_EQ("86400", acma->second); +} + +TEST_F(HttpServerTest, should_handle_routings_with_just_a_regex) +{ + auto handler = [&](SessionPtr session, RequestPtr request) -> bool { + ResponsePtr resp = make_unique(status::ok, "Done"); + session->writeResponse(std::move(resp)); + return true; + }; + + m_server->addRouting({boost::beast::http::verb::get, regex("/.+"), handler}); + m_server->addRouting({http::verb::put, "/{device}?timestamp={timestamp}", handler}); + + start(); + startClient(); + + m_client->spawnRequest(http::verb::options, "/file.xsd"); + ASSERT_TRUE(m_client->m_done); + EXPECT_EQ(int(http::status::no_content), m_client->m_status); + + auto allow = m_client->m_fields.find("Allow"); + ASSERT_NE(m_client->m_fields.end(), allow); + EXPECT_NE(string::npos, allow->second.find("GET")); + EXPECT_NE(string::npos, allow->second.find("OPTIONS")); + EXPECT_NE(string::npos, allow->second.find("PUT")); + EXPECT_EQ(string::npos, allow->second.find("POST")); + EXPECT_EQ(string::npos, allow->second.find("DELETE")); + + auto acam = m_client->m_fields.find("Access-Control-Allow-Methods"); + ASSERT_NE(m_client->m_fields.end(), acam); + EXPECT_EQ(allow->second, acam->second); + + auto acah = m_client->m_fields.find("Access-Control-Allow-Headers"); + ASSERT_NE(m_client->m_fields.end(), acah); + + auto acma = m_client->m_fields.find("Access-Control-Max-Age"); + ASSERT_NE(m_client->m_fields.end(), acma); + EXPECT_EQ("86400", acma->second); +} + const string CertFile(TEST_RESOURCE_DIR "/user.crt"); const string KeyFile {TEST_RESOURCE_DIR "/user.key"}; const string DhFile {TEST_RESOURCE_DIR "/dh2048.pem"}; diff --git a/test_package/json_helper.hpp b/test_package/json_helper.hpp index 71d74fb08..72dcdbe4d 100644 --- a/test_package/json_helper.hpp +++ b/test_package/json_helper.hpp @@ -17,12 +17,12 @@ #include -constexpr inline nlohmann::json::size_type operator"" _S(unsigned long long v) +constexpr inline nlohmann::json::size_type operator""_S(unsigned long long v) { return static_cast(v); } -inline std::string operator"" _S(const char *v, std::size_t) { return std::string(v); } +inline std::string operator""_S(const char *v, std::size_t) { return std::string(v); } static inline nlohmann::json find(nlohmann::json &array, const char *path, const char *value) { diff --git a/test_package/json_mapping_test.cpp b/test_package/json_mapping_test.cpp index 7b9e8f31c..975e843f2 100644 --- a/test_package/json_mapping_test.cpp +++ b/test_package/json_mapping_test.cpp @@ -135,7 +135,7 @@ class JsonMappingTest : public testing::Test std::map m_devices; }; -inline DataSetEntry operator"" _E(const char *c, std::size_t) { return DataSetEntry(c); } +inline DataSetEntry operator""_E(const char *c, std::size_t) { return DataSetEntry(c); } using namespace date::literals; /// @test verify the json mapper can map an object with a timestamp and a series of observations diff --git a/test_package/json_printer_stream_test.cpp b/test_package/json_printer_stream_test.cpp index 8ae04ad3f..70e252580 100644 --- a/test_package/json_printer_stream_test.cpp +++ b/test_package/json_printer_stream_test.cpp @@ -123,14 +123,14 @@ class JsonPrinterStreamTest : public testing::Test std::list m_devices; }; -Properties operator"" _value(unsigned long long value) +Properties operator""_value(unsigned long long value) { return Properties {{"VALUE", int64_t(value)}}; } -Properties operator"" _value(long double value) { return Properties {{"VALUE", double(value)}}; } +Properties operator""_value(long double value) { return Properties {{"VALUE", double(value)}}; } -Properties operator"" _value(const char *value, size_t s) +Properties operator""_value(const char *value, size_t s) { return Properties {{"VALUE", string(value)}}; } diff --git a/test_package/kinematics_test.cpp b/test_package/kinematics_test.cpp index 15fe9aa10..119e9705f 100644 --- a/test_package/kinematics_test.cpp +++ b/test_package/kinematics_test.cpp @@ -36,7 +36,7 @@ using namespace std; using namespace mtconnect; using namespace mtconnect::entity; -inline DataSetEntry operator"" _E(const char *c, std::size_t) { return DataSetEntry(c); } +inline DataSetEntry operator""_E(const char *c, std::size_t) { return DataSetEntry(c); } // main int main(int argc, char *argv[]) diff --git a/test_package/routing_test.cpp b/test_package/routing_test.cpp index 3235cea32..489458af7 100644 --- a/test_package/routing_test.cpp +++ b/test_package/routing_test.cpp @@ -304,3 +304,59 @@ TEST_F(RoutingTest, simple_put_with_trailing_slash) ASSERT_TRUE(r.matches(0, request)); ASSERT_EQ("ADevice", get(request->m_parameters["device"])); } + +TEST_F(RoutingTest, matchesPath_matches_simple_path) +{ + Routing r(verb::get, "/probe", m_func); + + EXPECT_TRUE(r.matchesPath("/probe")); + EXPECT_TRUE(r.matchesPath("/probe/")); + EXPECT_FALSE(r.matchesPath("/sample")); + EXPECT_FALSE(r.matchesPath("/probe/extra")); +} + +TEST_F(RoutingTest, matchesPath_matches_path_with_parameter) +{ + Routing r(verb::get, "/{device}/probe", m_func); + + EXPECT_TRUE(r.matchesPath("/ABC123/probe")); + EXPECT_TRUE(r.matchesPath("/mydevice/probe")); + EXPECT_FALSE(r.matchesPath("/probe")); + EXPECT_FALSE(r.matchesPath("/dev/probe/extra")); +} + +TEST_F(RoutingTest, matchesPath_ignores_verb) +{ + Routing getRoute(verb::get, "/asset/{id}", m_func); + Routing putRoute(verb::put, "/asset/{id}", m_func); + Routing deleteRoute(verb::delete_, "/asset/{id}", m_func); + + // matchesPath should match regardless of the routing's verb + EXPECT_TRUE(getRoute.matchesPath("/asset/A1")); + EXPECT_TRUE(putRoute.matchesPath("/asset/A1")); + EXPECT_TRUE(deleteRoute.matchesPath("/asset/A1")); + + // Different paths should not match + EXPECT_FALSE(getRoute.matchesPath("/probe")); + EXPECT_FALSE(putRoute.matchesPath("/probe")); + EXPECT_FALSE(deleteRoute.matchesPath("/probe")); +} + +TEST_F(RoutingTest, matchesPath_works_with_regex_routing) +{ + Routing r(verb::get, regex("/.+"), m_func); + + EXPECT_TRUE(r.matchesPath("/anything")); + EXPECT_TRUE(r.matchesPath("/some/deep/path")); + EXPECT_FALSE(r.matchesPath("/")); +} + +TEST_F(RoutingTest, matchesPath_with_query_parameters_in_pattern) +{ + Routing r(verb::get, "/{device}/sample?from={unsigned_integer}&count={integer:100}", m_func); + + // matchesPath only checks the path component, query params in the pattern don't affect it + EXPECT_TRUE(r.matchesPath("/ABC123/sample")); + EXPECT_TRUE(r.matchesPath("/device1/sample/")); + EXPECT_FALSE(r.matchesPath("/sample")); +} diff --git a/test_package/table_test.cpp b/test_package/table_test.cpp index d27b14edd..7df11460a 100644 --- a/test_package/table_test.cpp +++ b/test_package/table_test.cpp @@ -80,8 +80,8 @@ class TableTest : public testing::Test std::unique_ptr m_agentTestHelper; }; -inline DataSetEntry operator"" _E(const char *c, std::size_t) { return DataSetEntry(c); } -inline TableCell operator"" _C(const char *c, std::size_t) { return TableCell(c); } +inline DataSetEntry operator""_E(const char *c, std::size_t) { return DataSetEntry(c); } +inline TableCell operator""_C(const char *c, std::size_t) { return TableCell(c); } TEST_F(TableTest, DataItem) { diff --git a/test_package/tls_http_server_test.cpp b/test_package/tls_http_server_test.cpp index 262e25081..f786338fa 100644 --- a/test_package/tls_http_server_test.cpp +++ b/test_package/tls_http_server_test.cpp @@ -179,7 +179,7 @@ class Client m_bodyParser->on_chunk_header(m_headerHandler); auto body = [this](std::uint64_t remain, boost::string_view body, - boost::system::error_code& ev) -> unsigned long { + boost::system::error_code& ev) -> std::size_t { // cout << "Reading body" << endl; m_count++; m_ec = ev; diff --git a/test_package/topic_mapping_test.cpp b/test_package/topic_mapping_test.cpp index 19b928af2..c10582dba 100644 --- a/test_package/topic_mapping_test.cpp +++ b/test_package/topic_mapping_test.cpp @@ -122,7 +122,7 @@ class TopicMappingTest : public testing::Test std::map m_devices; }; -inline DataSetEntry operator"" _E(const char *c, std::size_t) { return DataSetEntry(c); } +inline DataSetEntry operator""_E(const char *c, std::size_t) { return DataSetEntry(c); } TEST_F(TopicMappingTest, should_find_data_item_for_topic) { diff --git a/test_package/xml_printer_test.cpp b/test_package/xml_printer_test.cpp index 287344fb1..787b593f1 100644 --- a/test_package/xml_printer_test.cpp +++ b/test_package/xml_printer_test.cpp @@ -46,7 +46,7 @@ int main(int argc, char *argv[]) return RUN_ALL_TESTS(); } -Properties operator"" _value(const char *value, size_t s) +Properties operator""_value(const char *value, size_t s) { return Properties {{"VALUE", string(value)}}; }