From 09a2a45dc018f365ab1c90f4f5d5ba8dc20494de Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Sun, 23 Feb 2025 23:54:30 +0100 Subject: [PATCH 01/12] update uwebsocket dependency to 20.71.0 --- CMakeLists.txt | 18 ++++++++---------- examples/CMakeLists.txt | 13 ++----------- test/CMakeLists.txt | 11 ++--------- vcpkg-no-ssl.json | 4 ++-- vcpkg-with-ssl.json | 10 ++++------ vcpkg.json | 4 ++-- 6 files changed, 20 insertions(+), 40 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a91fa28..5b527d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,14 +48,7 @@ set(VCPKG_BUILD_TYPE ${CMAKE_BUILD_TYPE}) message("CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}") message("VCPKG_BUILD_TYPE: ${VCPKG_BUILD_TYPE}") -find_path(UWEBSOCKETS_INCLUDE_DIRS "uwebsockets/App.h") -message("µWebsockets include dir: ${UWEBSOCKETS_INCLUDE_DIRS}") -if(WIN32) - find_library(LIBUSOCKETS_STATIC uSockets.lib) -else(WIN32) - find_library(LIBUSOCKETS_STATIC libuSockets.a) -endif(WIN32) -message(${LIBUSOCKETS_STATIC}) +find_package(unofficial-uwebsockets CONFIG REQUIRED) find_path(MDNS_INCLUDE_DIRS "mdns.h") message("mdns include dir: ${MDNS_INCLUDE_DIRS}") @@ -63,8 +56,6 @@ message("mdns include dir: ${MDNS_INCLUDE_DIRS}") find_package(mdns REQUIRED) find_package(nlohmann_json 3.11.2 REQUIRED) find_package(nlohmann_json_schema_validator REQUIRED) -find_package(libuv REQUIRED NO_MODULE) -find_package(ZLIB REQUIRED) if(WT_WITH_SSL) find_package(OpenSSL REQUIRED) @@ -83,6 +74,13 @@ if(WT_BUILD_TESTS) endif() add_library(webthing-cpp INTERFACE) + +target_link_libraries(webthing-cpp INTERFACE + nlohmann_json_schema_validator::validator + nlohmann_json::nlohmann_json + unofficial::uwebsockets::uwebsockets +) + target_include_directories(webthing-cpp INTERFACE $ $ diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 9826dbb..2b052b0 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -7,15 +7,11 @@ configure_file( ) set(LIBS_FOR_EXAMPLES - ${LIBUSOCKETS_STATIC} - nlohmann_json::nlohmann_json nlohmann_json_schema_validator::validator - ZLIB::ZLIB - $,libuv::uv_a,libuv::uv> + unofficial::uwebsockets::uwebsockets ) -set(INCLUDES_FOR_EXAMPLES ../include ${UWEBSOCKETS_INCLUDE_DIRS} ${MDNS_INCLUDE_DIRS}) - +set(INCLUDES_FOR_EXAMPLES ../include) function(create_example_binary cpp_file) cmake_path(GET cpp_file STEM target) @@ -26,11 +22,6 @@ function(create_example_binary cpp_file) target_include_directories("${target}" PRIVATE ${INCLUDES_FOR_EXAMPLES}) target_link_libraries("${target}" PRIVATE ${LIBS_FOR_EXAMPLES}) - - if(WT_WITH_SSL) - target_link_libraries("${target}" PRIVATE OpenSSL::SSL OpenSSL::Crypto) - endif(WT_WITH_SSL) - endfunction() create_example_binary(single-thing.cpp) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b80c342..c970fb2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -17,19 +17,12 @@ add_executable(tests set_property(TARGET tests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") -set(INCLUDES_FOR_TESTS ../include ${UWEBSOCKETS_INCLUDE_DIRS} ${MDNS_INCLUDE_DIRS}) +set(INCLUDES_FOR_TESTS ../include) target_compile_definitions(tests PRIVATE CATCH_CONFIG_ENABLE_ALL_STRINGMAKERS) target_include_directories(tests PRIVATE ${INCLUDES_FOR_TESTS}) target_link_libraries(tests PRIVATE Catch2::Catch2WithMain) -target_link_libraries(tests PRIVATE nlohmann_json::nlohmann_json) target_link_libraries(tests PRIVATE nlohmann_json_schema_validator::validator) -target_link_libraries(tests PRIVATE ${LIBUSOCKETS_STATIC}) -target_link_libraries(tests PRIVATE ZLIB::ZLIB) -target_link_libraries(tests PRIVATE $,libuv::uv_a,libuv::uv>) - -if(WT_WITH_SSL) - target_link_libraries(tests PRIVATE OpenSSL::SSL OpenSSL::Crypto) -endif(WT_WITH_SSL) +target_link_libraries(tests PRIVATE unofficial::uwebsockets::uwebsockets) include(CTest) include(Catch) diff --git a/vcpkg-no-ssl.json b/vcpkg-no-ssl.json index ad2763d..0506a2e 100644 --- a/vcpkg-no-ssl.json +++ b/vcpkg-no-ssl.json @@ -1,7 +1,7 @@ { "name": "webthing-cpp", "version-string": "1.1.0", - "builtin-baseline": "c82f74667287d3dc386bce81e44964370c91a289", + "builtin-baseline": "d5ec528843d29e3a52d745a64b469f810b2cedbf", "dependencies": [ "catch2", "json-schema-validator", @@ -28,7 +28,7 @@ }, { "name": "uwebsockets", - "version": "20.67.0" + "version": "20.71.0" } ] } \ No newline at end of file diff --git a/vcpkg-with-ssl.json b/vcpkg-with-ssl.json index aa70a01..b9f6386 100644 --- a/vcpkg-with-ssl.json +++ b/vcpkg-with-ssl.json @@ -1,20 +1,18 @@ { "name": "webthing-cpp", "version-string": "1.1.0", - "builtin-baseline": "c82f74667287d3dc386bce81e44964370c91a289", + "builtin-baseline": "d5ec528843d29e3a52d745a64b469f810b2cedbf", "dependencies": [ "catch2", "json-schema-validator", "mdns", "nlohmann-json", - "openssl", { - "name": "usockets", + "name": "uwebsockets", "features": [ "ssl" ] - }, - "uwebsockets" + } ], "overrides": [ { @@ -35,7 +33,7 @@ }, { "name": "uwebsockets", - "version": "20.67.0" + "version": "20.71.0" } ] } \ No newline at end of file diff --git a/vcpkg.json b/vcpkg.json index ad2763d..0506a2e 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,7 +1,7 @@ { "name": "webthing-cpp", "version-string": "1.1.0", - "builtin-baseline": "c82f74667287d3dc386bce81e44964370c91a289", + "builtin-baseline": "d5ec528843d29e3a52d745a64b469f810b2cedbf", "dependencies": [ "catch2", "json-schema-validator", @@ -28,7 +28,7 @@ }, { "name": "uwebsockets", - "version": "20.67.0" + "version": "20.71.0" } ] } \ No newline at end of file From b850522089885a6849dfdb62489e40ff274a4f5c Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Mon, 24 Feb 2025 02:10:08 +0100 Subject: [PATCH 02/12] add code coverage report --- build.sh | 5 ++++- test/CMakeLists.txt | 9 +++++++++ test/coverage.sh | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 test/coverage.sh diff --git a/build.sh b/build.sh index 86919a1..134d150 100644 --- a/build.sh +++ b/build.sh @@ -16,11 +16,14 @@ fi if [[ "${@#release}" = "$@" ]] then build_type="Debug" + code_coverage="ON" else build_type="Release" + code_coverage="OFF" rm -rf $build_dir fi echo "project build type: $build_type" +echo "project code coverage: $code_coverage" if [[ "${@#with_ssl}" = "$@" ]] then @@ -34,7 +37,7 @@ echo "project SSL support: $ssl_support" cp $vcpkg_file vcpkg.json -cmake -B build -S . -D"WT_WITH_SSL=$ssl_support" -D"CMAKE_BUILD_TYPE=$build_type" -D"CMAKE_TOOLCHAIN_FILE=$toolchain_file" -D"CMAKE_MAKE_PROGRAM:PATH=make" -D"CMAKE_CXX_COMPILER=g++" +cmake -B build -S . -D"WT_WITH_SSL=$ssl_support" -D"CMAKE_BUILD_TYPE=$build_type" -D"WT_ENABLE_COVERAGE=$code_coverage" -D"CMAKE_TOOLCHAIN_FILE=$toolchain_file" -D"CMAKE_MAKE_PROGRAM:PATH=make" -D"CMAKE_CXX_COMPILER=g++" cmake --build build --parallel $(nproc) ctest --test-dir build/test/ \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c970fb2..b1c27c1 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -24,6 +24,15 @@ target_link_libraries(tests PRIVATE Catch2::Catch2WithMain) target_link_libraries(tests PRIVATE nlohmann_json_schema_validator::validator) target_link_libraries(tests PRIVATE unofficial::uwebsockets::uwebsockets) +if(WT_ENABLE_COVERAGE) + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(tests PRIVATE --coverage) + target_link_options(tests PRIVATE --coverage) + else() + message(FATAL_ERROR "Coverage only supported with GCC/Clang") + endif() +endif() + include(CTest) include(Catch) catch_discover_tests(tests) \ No newline at end of file diff --git a/test/coverage.sh b/test/coverage.sh new file mode 100644 index 0000000..ab660c8 --- /dev/null +++ b/test/coverage.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +script_path=$(realpath "$0") +base_dir="$( dirname "$script_path" )/.." +build_dir="$base_dir/build" + +${base_dir}/build.sh clean debug + +echo "generate coverage.info:" +lcov --capture --directory . --output-file "${build_dir}/coverage.info" + +echo "generate filtered_coverage.info:" +lcov --extract "${build_dir}/coverage.info" '*/bw/webthing/*' --output-file "${build_dir}/filtered_coverage.info" + +echo "generate coverage_summary.json" +lcov --summary "${build_dir}/filtered_coverage.info" > "${build_dir}/coverage_summary.txt" +total_line_coverage=$(grep -E "lines" "${build_dir}/coverage_summary.txt" | awk '{print $2}') +echo "{\"total_line_coverage\": \"$total_line_coverage\"}" > "${build_dir}/coverage_summary.json" + +echo "generate coverage report:" + +html_prolog="${build_dir}/coverage_report_prolog.html" +rm -f $html_prolog + +echo '' >> $html_prolog +echo '' >> $html_prolog +echo '' >> $html_prolog +echo 'Webthing-CPP - @pagetitle@' >> $html_prolog +echo '' >> $html_prolog +echo '' >> $html_prolog +echo '' >> $html_prolog +echo '

' >> $html_prolog +echo 'Webthing-CPP: a modern CPP implementation of the WebThings API' >> $html_prolog +echo '


' >> $html_prolog + +rm -Rf "${build_dir}/coverage_report" +genhtml "${build_dir}/filtered_coverage.info" -o "${build_dir}/coverage_report" --html-prolog "${build_dir}/coverage_report_prolog.html" --rc genhtml_hi_limit=60 --rc genhtml_med_limit=50 + +echo "download coverage badge" +curl -o "${build_dir}/coverage_report/badge.svg" "https://img.shields.io/badge/coverage-${total_line_coverage}25-rgb%2850,201,85%29" From bd71bb24deb58fd7af06a50f8ab0821d6b5003cf Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Mon, 24 Feb 2025 02:11:37 +0100 Subject: [PATCH 03/12] add test for available action/event validation --- test/catch2/unit-tests/thing_tests.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/catch2/unit-tests/thing_tests.cpp b/test/catch2/unit-tests/thing_tests.cpp index 2e530d5..f0eaa1c 100644 --- a/test/catch2/unit-tests/thing_tests.cpp +++ b/test/catch2/unit-tests/thing_tests.cpp @@ -11,7 +11,6 @@ using namespace bw::webthing; TEST_CASE( "Webthing thing context is configurable", "[context][thing]" ) { - auto sut = std::make_shared("uri::test.id", "my-test-thing"); REQUIRE( sut->get_context() == WEBTHINGS_IO_CONTEXT ); // default @@ -19,6 +18,17 @@ TEST_CASE( "Webthing thing context is configurable", "[context][thing]" ) REQUIRE( sut->get_context() == "https://some.custom/context" ); } +TEST_CASE( "Webthing thing validates description of available events", "[event][thing]" ) +{ + auto types = std::vector{"test-type"}; + auto sut = std::make_shared("uri::test.id", "my-test-thing", types, "This is the description of my-test-thing"); + + REQUIRE_NOTHROW(sut->add_available_event("test-event-a", {{"description", "Event A"}, {"type","string"}})); + + REQUIRE_THROWS_MATCHES(sut->add_available_event("test-event-b", {"\"JUST AN JSON STRING BUT NO OBJECT\""}), + EventError, Catch::Matchers::Message("Event metadata must be encoded as json object.")); +} + TEST_CASE( "Webthing thing stores all published events", "[event][thing]" ) { auto types = std::vector{"test-type"}; @@ -47,6 +57,17 @@ TEST_CASE( "Webthing thing stores all published events", "[event][thing]" ) REQUIRE( sut->get_event_descriptions("test-event-missing").size() == 0 ); } +TEST_CASE( "Webthing thing validates description of available actions", "[action][thing]" ) +{ + auto types = std::vector{"test-type"}; + auto sut = std::make_shared("uri::test.id", "my-test-thing", types, "This is the description of my-test-thing"); + + REQUIRE_NOTHROW(sut->add_available_action("test-action-a", {{"title", "Action A"}}, nullptr)); + + REQUIRE_THROWS_MATCHES(sut->add_available_action("test-action-b", {"\"JUST AN JSON STRING BUT NO OBJECT\""}, nullptr), + ActionError, Catch::Matchers::Message("Action metadata must be encoded as json object.")); +} + TEST_CASE( "Webthing things performes actions", "[action][thing]" ) { struct TestThing : public Thing From c12eeb29d7f79ea25cb750f5dcf0be7b5d98f271 Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Tue, 4 Mar 2025 21:15:36 +0100 Subject: [PATCH 04/12] refactor server, add tests, inline webthing helper functions --- include/bw/webthing/server.hpp | 25 +- include/bw/webthing/thing.hpp | 4 +- include/bw/webthing/webthing.hpp | 24 +- test/CMakeLists.txt | 2 + test/catch2/unit-tests/action_tests.cpp | 69 ++- test/catch2/unit-tests/property_tests.cpp | 13 + test/catch2/unit-tests/server_tests.cpp | 541 +++++++++++++++++++++- test/catch2/unit-tests/thing_tests.cpp | 6 + vcpkg-no-ssl.json | 1 + vcpkg-with-ssl.json | 1 + vcpkg.json | 1 + 11 files changed, 634 insertions(+), 53 deletions(-) diff --git a/include/bw/webthing/server.hpp b/include/bw/webthing/server.hpp index 7e847f0..f903202 100644 --- a/include/bw/webthing/server.hpp +++ b/include/bw/webthing/server.hpp @@ -91,6 +91,7 @@ struct MultipleThings : public ThingContainer{ class WebThingServer { +public: struct Builder { Builder(ThingContainer things) @@ -133,11 +134,6 @@ class WebThingServer return *this; } - Builder& limit_memory() - { - return *this; - } - WebThingServer build() { return WebThingServer(things_, port_, hostname_, base_path_, @@ -160,7 +156,6 @@ class WebThingServer }; -public: struct Response { Response(uWS::HttpRequest* req, uwsHttpResponse* res) @@ -434,9 +429,7 @@ class WebThingServer else if(v.is_string()) prop_setter(v.get()); else if(v.is_number_integer()) - prop_setter(v.get()); - else if(v.is_number_unsigned()) - prop_setter(v.get()); + prop_setter(v.get()); else if(v.is_number_float()) prop_setter(v.get()); else @@ -528,6 +521,16 @@ class WebThingServer return name; } + int get_port() const + { + return port; + } + + std::string get_base_path() const + { + return base_path; + } + uWebsocketsApp* get_web_server() const { return web_server.get(); @@ -820,11 +823,9 @@ class WebThingServer if(v.is_boolean()) prop_setter(v.get()); else if(v.is_string()) - prop_setter(v.get()); + prop_setter(v.get()); else if(v.is_number_integer()) prop_setter(v.get()); - else if(v.is_number_unsigned()) - prop_setter(v.get()); else if(v.is_number_float()) prop_setter(v.get()); else diff --git a/include/bw/webthing/thing.hpp b/include/bw/webthing/thing.hpp index 693cd0b..4a4c6cc 100644 --- a/include/bw/webthing/thing.hpp +++ b/include/bw/webthing/thing.hpp @@ -132,7 +132,7 @@ class Thing return pds; } - // Get the thing's actions a json array + // Get the thing's actions as json array // action_name -- Optional action name to get description for json get_action_descriptions(std::optional action_name = std::nullopt) const { @@ -146,7 +146,7 @@ class Thing return descriptions; } - // Get the thing's events as a json array. + // Get the thing's events as json array. // event_name -- Optional event name to get description for json get_event_descriptions(const std::optional& event_name = std::nullopt) const { diff --git a/include/bw/webthing/webthing.hpp b/include/bw/webthing/webthing.hpp index ad948f6..8b46622 100644 --- a/include/bw/webthing/webthing.hpp +++ b/include/bw/webthing/webthing.hpp @@ -20,7 +20,7 @@ namespace bw::webthing { -std::shared_ptr make_thing(std::string id, std::string title, std::vector type, std::string description) +inline std::shared_ptr make_thing(std::string id, std::string title, std::vector type, std::string description) { if(id == "") id = "uuid:" + generate_uuid(); @@ -29,7 +29,7 @@ std::shared_ptr make_thing(std::string id, std::string title, std::vector return std::make_shared(id, title, type, description); } -std::shared_ptr make_thing(std::string id = "", std::string title = "", std::string type = "", std::string description = "") +inline std::shared_ptr make_thing(std::string id = "", std::string title = "", std::string type = "", std::string description = "") { std::vector types; if(type != "") @@ -72,43 +72,43 @@ template std::shared_ptr> link_property(std::shared_ptradd_available_event(name, metadata); } -void link_event(std::shared_ptr thing, std::string name, json metadata = json::object()) +inline void link_event(std::shared_ptr thing, std::string name, json metadata = json::object()) { link_event(thing.get(), name, metadata); } -std::shared_ptr emit_event(Thing* thing, std::string name, std::optional data = std::nullopt) +inline std::shared_ptr emit_event(Thing* thing, std::string name, std::optional data = std::nullopt) { auto event = std::make_shared(thing, name, data); thing->add_event(event); return event; } -std::shared_ptr emit_event(std::shared_ptr thing, std::string name, std::optional data = std::nullopt) +inline std::shared_ptr emit_event(std::shared_ptr thing, std::string name, std::optional data = std::nullopt) { return emit_event(thing.get(), name, data); } -std::shared_ptr emit_event(Thing* thing, Event&& event) +inline std::shared_ptr emit_event(Thing* thing, Event&& event) { auto event_ptr = std::make_shared(event); thing->add_event(event_ptr); return event_ptr; } -std::shared_ptr emit_event(std::shared_ptr thing, Event&& event) +inline std::shared_ptr emit_event(std::shared_ptr thing, Event&& event) { auto event_ptr = std::make_shared(event); thing->add_event(event_ptr); return event_ptr; } -void link_action(Thing* thing, std::string action_name, json metadata, +inline void link_action(Thing* thing, std::string action_name, json metadata, std::function perform_action = nullptr, std::function cancel_action = nullptr) { Thing::ActionSupplier action_supplier = [thing, action_name, perform_action, cancel_action](auto input){ @@ -120,19 +120,19 @@ void link_action(Thing* thing, std::string action_name, json metadata, thing->add_available_action(action_name, metadata, std::move(action_supplier)); } -void link_action(std::shared_ptr thing, std::string action_name, json metadata, +inline void link_action(std::shared_ptr thing, std::string action_name, json metadata, std::function perform_action = nullptr, std::function cancel_action = nullptr) { link_action(thing.get(), action_name, metadata, perform_action, cancel_action); } -void link_action(Thing* thing, std::string action_name, +inline void link_action(Thing* thing, std::string action_name, std::function perform_action = nullptr, std::function cancel_action = nullptr) { link_action(thing, action_name, json::object(), perform_action, cancel_action); } -void link_action(std::shared_ptr thing, std::string action_name, +inline void link_action(std::shared_ptr thing, std::string action_name, std::function perform_action = nullptr, std::function cancel_action = nullptr) { link_action(thing.get(), action_name, perform_action, cancel_action); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b1c27c1..9cb0298 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,4 +1,5 @@ find_package(Catch2 3 REQUIRED) +find_package(cpr CONFIG REQUIRED) add_executable(tests "catch2/unit-tests/action_tests.cpp" @@ -21,6 +22,7 @@ set(INCLUDES_FOR_TESTS ../include) target_compile_definitions(tests PRIVATE CATCH_CONFIG_ENABLE_ALL_STRINGMAKERS) target_include_directories(tests PRIVATE ${INCLUDES_FOR_TESTS}) target_link_libraries(tests PRIVATE Catch2::Catch2WithMain) +target_link_libraries(tests PRIVATE cpr::cpr) target_link_libraries(tests PRIVATE nlohmann_json_schema_validator::validator) target_link_libraries(tests PRIVATE unofficial::uwebsockets::uwebsockets) diff --git a/test/catch2/unit-tests/action_tests.cpp b/test/catch2/unit-tests/action_tests.cpp index 9ffa465..2314af4 100644 --- a/test/catch2/unit-tests/action_tests.cpp +++ b/test/catch2/unit-tests/action_tests.cpp @@ -53,28 +53,57 @@ TEST_CASE( "Webthing specifies action status message fromat", "[action][json]" ) SCENARIO( "actions have a stateful lifecycle", "[action]" ) { + FIXED_TIME_SCOPED("2025-02-17T02:34:56.000+00:00"); + + GIVEN("a custom action") { struct CustomAction : public Action { - CustomAction() : Action("abc123", { + CustomAction(void* thing_void_ptr) + : Action("abc123", { /*notify_thing*/[](json j){ - std::cout << "THING GOT " << j << std::endl; + logger::info("THING GOT " + j.dump()); }, /*perform_action*/[&]{ - std::cout << "CustomAction perform action with input:" << get_input().value_or(json()) << std::endl; - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - work_done = true; + logger::info("CustomAction perform action with input:" + get_input().value_or(json()).dump()); + + auto start_time = std::chrono::steady_clock::now(); + auto max_duration = std::chrono::milliseconds(200); + + while (!cancel_work && std::chrono::steady_clock::now() - start_time < max_duration) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + work_done = !cancel_work; + }, + /*canel_action*/[&]{ + logger::info("CustomAction cancel action" + get_input().value_or(json()).dump()); + cancel_work = true; + }, + /*get_thing*/[&]{ + return thing_ptr; } }, "my-custom-action", json({"a", "b", "c"})) + , thing_ptr(thing_void_ptr) {} + void* thing_ptr = nullptr; + bool cancel_work = false; bool work_done = false; }; - - CustomAction action; + + struct TestThing + { + } thing; + CustomAction action(&thing); REQUIRE( action.get_id() == "abc123" ); + REQUIRE( action.get_name() == "my-custom-action" ); REQUIRE( action.get_status() == "created" ); + REQUIRE( action.get_href() == "/actions/my-custom-action/abc123" ); + REQUIRE( action.get_thing() == &thing ); + REQUIRE( action.get_time_requested() == "2025-02-17T02:34:56.000+00:00" ); + REQUIRE_FALSE( action.get_time_completed() ); WHEN("action is performed successfully") { @@ -88,10 +117,34 @@ SCENARIO( "actions have a stateful lifecycle", "[action]" ) if(t.joinable()) t.join(); - THEN("action will be considered completed") + THEN("action will be considered completed and work to be done") { REQUIRE( action.get_status() == "completed" ); REQUIRE( action.work_done ); + REQUIRE( action.get_time_completed() == "2025-02-17T02:34:56.000+00:00" ); + } + } + + WHEN("action was canceled") + { + std::thread t([&](){ + action.start(); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + REQUIRE( action.get_status() == "pending" ); + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + action.cancel(); + + if(t.joinable()) + t.join(); + + THEN("action will be considered completed but work to be unfinsihed") + { + REQUIRE( action.get_status() == "completed" ); + REQUIRE_FALSE( action.work_done ); + REQUIRE( action.get_time_completed() == "2025-02-17T02:34:56.000+00:00" ); } } } diff --git a/test/catch2/unit-tests/property_tests.cpp b/test/catch2/unit-tests/property_tests.cpp index 82e3df6..37e3a58 100644 --- a/test/catch2/unit-tests/property_tests.cpp +++ b/test/catch2/unit-tests/property_tests.cpp @@ -66,6 +66,19 @@ TEST_CASE( "Properties can be updated", "[property]" ) REQUIRE( v1->get() == "Бennö 森林" ); } +TEST_CASE( "Properties meta data must be encoded as json object", "[property]" ) +{ + auto val = create_value("a_string_value"); + auto prop = create_proptery("test-prop", val, json{{"title", "some-test-property"}}); + + REQUIRE( prop->get_metadata()["title"] == "some-test-property" ); + REQUIRE( *prop->get_value() == "a_string_value" ); + + REQUIRE_THROWS_MATCHES(create_proptery("test-prop", val, json{"some","data","as","array"}), + PropertyError, + Catch::Matchers::Message("Only json::object is allowed as meta data.")); +} + TEST_CASE( "Properties value type can't be changed", "[property]" ) { auto val = create_value("a_string_value"); diff --git a/test/catch2/unit-tests/server_tests.cpp b/test/catch2/unit-tests/server_tests.cpp index a00b9da..9304c62 100644 --- a/test/catch2/unit-tests/server_tests.cpp +++ b/test/catch2/unit-tests/server_tests.cpp @@ -3,39 +3,542 @@ // SPDX-License-Identifier: MIT #include +#include +#include #include using namespace bw::webthing; -TEST_CASE( "It can host a single thing" ) +void test_running_server(WebThingServer::Builder& builder, std::function test_callback) { - Thing thing("uri:test:1", "single-thing"); - auto thing_container = SingleThing(&thing); + logger::set_level(log_level::trace); + auto server = builder.build(); + REQUIRE(server.get_web_server() != nullptr); + + int port = server.get_port(); + std::string base_path = server.get_base_path(); + std::exception_ptr thread_exception = nullptr; + + auto t = std::thread([&](){ - auto server = WebThingServer::host(thing_container).port(57456).build(); - REQUIRE(server.get_name() == "single-thing"); + try + { + std::string base_url = "http://localhost:" + std::to_string(port) + base_path; + test_callback(&server, base_url); + } catch (...) { + thread_exception = std::current_exception(); + } - auto t = std::thread([&server]{ - std::this_thread::sleep_for(std::chrono::seconds(1)); server.stop(); }); + server.start(); t.join(); + + if (thread_exception) { + std::rethrow_exception(thread_exception); + } +}; + +TEST_CASE( "It can host a single thing", "[server]" ) +{ + auto thing = make_thing("uri:test:1", "single-thing"); + link_property(thing, "brightness", 50, { + {"@type", "BrightnessProperty"}, + {"title", "Brightness"}, + {"type", "integer"}, + {"description", "The level of light from 0-100"}, + {"minimum", 0}, + {"maximum", 100}, + {"unit", "percent"}}); + + auto thing_container = SingleThing(thing.get()); + auto builder = WebThingServer::host(thing_container).port(57456); + + test_running_server(builder, [](WebThingServer* server, const std::string& base_url) + { + REQUIRE(server->get_name() == "single-thing"); + auto res = cpr::Get(cpr::Url{base_url}); + REQUIRE(res.status_code == 200); + auto td = json::parse(res.text); + REQUIRE(td["title"] == "single-thing"); + + res = cpr::Get(cpr::Url{base_url + "/properties"}); + REQUIRE(res.status_code == 200); + auto props = json::parse(res.text); + REQUIRE(props["brightness"] == 50); + + res = cpr::Put( + cpr::Url{base_url + "/properties/brightness"}, + cpr::Body{json{{"brightness", 42}}.dump()} + ); + REQUIRE(res.status_code == 200); + + res = cpr::Get(cpr::Url{base_url + "/properties/brightness"}); + REQUIRE(res.status_code == 200); + props = json::parse(res.text); + REQUIRE(props["brightness"] == 42); + + res = cpr::Put( + cpr::Url{base_url + "/properties/brightness"} /*no body*/ + ); + REQUIRE(res.status_code == 400); // bad request + + // property name missing in payload + res = cpr::Put( + cpr::Url{base_url + "/properties/brightness"}, + cpr::Body{json{123}.dump()} + ); + REQUIRE(res.status_code == 400); // bad request + + res = cpr::Get(cpr::Url{base_url + "/properties/not-existing-property"}); + REQUIRE(res.status_code == 404); // not found + + res = cpr::Put( + cpr::Url{base_url + "/properties/not-existing-property"}, + cpr::Body{json{{"not-existing-property", 123}}.dump()} + ); + REQUIRE(res.status_code == 404); // not found + }); } -TEST_CASE( "It can host a multiple things" ) +TEST_CASE( "It can host multiple things", "[server]" ) { - Thing thing_a("uri:test:a", "thing-a"); - Thing thing_b("uri:test:b", "thing-b"); - auto thing_container = MultipleThings({&thing_a, &thing_b}, "things-a-and-b"); + auto thing_a = make_thing("uri:test:a", "thing-a"); + + link_property(thing_a, "boolean-prop", true, { + {"title", "Bool Property"}, + {"type", "boolean"} + }); + + link_property(thing_a, "double-prop", 42.13, { + {"title", "Double Property"}, + {"type", "number"} + }); - auto server = WebThingServer::host(thing_container).port(57123).build(); - REQUIRE(server.get_name() == "things-a-and-b"); + link_property(thing_a, "string-prop", std::string("the-value"), { + {"title", "String Property"}, + {"type", "string"} + }); - auto t = std::thread([&server]{ - std::this_thread::sleep_for(std::chrono::seconds(1)); - server.stop(); + auto thing_b = make_thing("uri:test:b", "thing-b"); + + link_property(thing_b, "object-prop", json{{"key", "value"}}, { + {"title", "Object Property"}, + {"type", "object"} }); - server.start(); - t.join(); -} \ No newline at end of file + + link_property(thing_b, "array-prop", json{"some", "values", 42}, { + {"title", "Array Property"}, + {"type", "array"} + }); + + auto thing_container = MultipleThings({thing_a.get(), thing_b.get()}, "things-a-and-b"); + + auto builder = WebThingServer::host(thing_container).port(57123); + test_running_server(builder, [](WebThingServer* server, const std::string& base_url) + { + REQUIRE(server->get_name() == "things-a-and-b"); + auto res = cpr::Get(cpr::Url{base_url}); + REQUIRE(res.status_code == 200); + + res = cpr::Get(cpr::Url{base_url + "/0"}); + REQUIRE(res.status_code == 200); + auto ta = json::parse(res.text); + REQUIRE(ta["title"] == "thing-a"); + + res = cpr::Get(cpr::Url{base_url + "/1"}); + REQUIRE(res.status_code == 200); + auto tb = json::parse(res.text); + REQUIRE(tb["title"] == "thing-b"); + + res = cpr::Put( + cpr::Url{base_url + "/0/properties/boolean-prop"}, + cpr::Body{json{{"boolean-prop", false}}.dump()} + ); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text)["boolean-prop"] == false); + + res = cpr::Put( + cpr::Url{base_url + "/0/properties/double-prop"}, + cpr::Body{json{{"double-prop", 24.0}}.dump()} + ); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text)["double-prop"] == 24.0); + + res = cpr::Put( + cpr::Url{base_url + "/0/properties/string-prop"}, + cpr::Body{json{{"string-prop", "the-updated-value"}}.dump()} + ); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text)["string-prop"] == "the-updated-value"); + + res = cpr::Put( + cpr::Url{base_url + "/1/properties/object-prop"}, + cpr::Body{json{{"object-prop", {{"key", "updated-value"}}}}.dump()} + ); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text)["object-prop"] == json{{"key", "updated-value"}}); + + res = cpr::Put( + cpr::Url{base_url + "/1/properties/array-prop"}, + cpr::Body{json{{"array-prop", {"a", "b", "c", 42}}}.dump()} + ); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text)["array-prop"] == json{"a", "b", "c", 42}); + + // not exsiting thing + res = cpr::Get(cpr::Url{base_url + "/42"}); + REQUIRE(res.status_code == 404); + + res = cpr::Get(cpr::Url{base_url + "/42/properties"}); + REQUIRE(res.status_code == 404); + + res = cpr::Get(cpr::Url{base_url + "/42/properties/test-property"}); + REQUIRE(res.status_code == 404); + + res = cpr::Put( + cpr::Url{base_url + "/42/properties/test-property"}, + cpr::Body{json{{"key", "value"}}.dump()} + ); + REQUIRE(res.status_code == 404); + }); +} + +TEST_CASE( "It handles invalid requests", "[server]" ) +{ + Thing thing("uri:test", "single-thing"); + auto thing_container = SingleThing(&thing); + + auto builder = WebThingServer::host(thing_container).port(57111); + test_running_server(builder, [](WebThingServer* server, const std::string& base_url) + { + auto res = cpr::Get(cpr::Url{base_url}); + REQUIRE(res.status_code == 200); + + res = cpr::Put(cpr::Url{base_url}); + REQUIRE(res.status_code == 405); // method not allowed + + res = cpr::Get(cpr::Url{base_url + "/some-not-existing-resource"}); + REQUIRE(res.status_code == 405); // method not allowed + + res = cpr::Get(cpr::Url{base_url + "/properties/not-existing-property"}); + REQUIRE(res.status_code == 404); // not + }); +} + +TEST_CASE( "It supports preflight requests", "[server]" ) +{ + Thing thing("uri:test", "single-thing"); + auto thing_container = SingleThing(&thing); + + auto builder = WebThingServer::host(thing_container).port(57222); + test_running_server(builder, [](WebThingServer* server, const std::string& base_url) + { + auto res = cpr::Options(cpr::Url{base_url}); + REQUIRE(res.status_code == 204); // no content + }); +} + +TEST_CASE( "It redirects urls with trailing slash to corresponding url without trailing slash", "[server]" ) +{ + Thing thing("uri:test", "single-thing"); + auto thing_container = SingleThing(&thing); + + auto builder = WebThingServer::host(thing_container).port(57222); + test_running_server(builder, [](WebThingServer* server, const std::string& base_url) + { + auto res = cpr::Get(cpr::Url{base_url + "/properties/"}, cpr::Redirect{false}); + REQUIRE(res.status_code == 301); // moved permanently + REQUIRE(res.header["Location"] == base_url + "/properties"); + }); +} + +TEST_CASE( "It supports custom host name", "[server]" ) +{ + Thing thing("uri:test", "single-thing"); + auto thing_container = SingleThing(&thing); + + // test with enabled host validation + auto builder_host_validation = WebThingServer::host(thing_container) + .hostname("custom-host") + .disable_host_validation(false) + .port(57333); + + test_running_server(builder_host_validation, [](WebThingServer* server, const std::string& base_url) + { + auto res = cpr::Get(cpr::Url{base_url}, cpr::Header{{"Host","custom-host"}}); + REQUIRE(res.status_code == 200); + + res = cpr::Get(cpr::Url{base_url}, cpr::Header{{"Host","unknown-host"}}); + REQUIRE(res.status_code == 403); // forbidden + }); + + // test with disabled host validation + auto builder_no_host_validation = WebThingServer::host(thing_container) + .hostname("custom-host") + .disable_host_validation(true) + .port(57334); + + test_running_server(builder_no_host_validation, [](WebThingServer* server, const std::string& base_url) + { + auto res = cpr::Get(cpr::Url{base_url}, cpr::Header{{"Host","custom-host"}}); + REQUIRE(res.status_code == 200); + + res = cpr::Get(cpr::Url{base_url}, cpr::Header{{"Host","unknown-host"}}); + REQUIRE(res.status_code == 200); + }); +} + +TEST_CASE( "It supports custom base path", "[server]" ) +{ + Thing thing("uri:test", "single-thing"); + auto thing_container = SingleThing(&thing); + + auto builder = WebThingServer::host(thing_container) + .base_path("/custom-base") + .port(57444); + + test_running_server(builder, [](WebThingServer* server, const std::string& base_url) + { + auto res = cpr::Get(cpr::Url{base_url}); + REQUIRE(res.status_code == 200); + REQUIRE_THAT(base_url, Catch::Matchers::EndsWith("/custom-base")); + REQUIRE_THAT(json::parse(res.text)["base"], Catch::Matchers::EndsWith("/custom-base")); + }); +} + +TEST_CASE( "It offers REST api for actions", "[server]" ) +{ + auto thing = make_thing("uri:test:1", "single-thing"); + + link_action(thing, "test-action", json{{"title", "Test Action"}}, []{ + logger::info("PERFORM TEST ACTION"); + }); + + link_action(thing, "throwing-test-action", json{ + {"title", "Throwing Test Action"}, + {"input", {{"type","number"}}} + }, []{ + logger::info("PERFORM THROWING TEST ACTION"); + throw std::runtime_error("ACTION FAILED BY INTEND"); + }); + + auto thing_container = MultipleThings({thing.get()}, "single-thing-in-multi-container"); + auto builder = WebThingServer::host(thing_container).port(57777); + + test_running_server(builder, [](WebThingServer* server, const std::string& base_url) + { + std::string thing_base_url = base_url + "/0"; + + REQUIRE(server->get_name() == "single-thing-in-multi-container"); + auto res = cpr::Get(cpr::Url{thing_base_url}); + REQUIRE(res.status_code == 200); + auto td = json::parse(res.text); + REQUIRE(td["actions"]["test-action"]["title"] == "Test Action"); + + res = cpr::Get(cpr::Url{thing_base_url + "/actions"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text).empty()); + + res = cpr::Get(cpr::Url{thing_base_url + "/actions/test-action"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text).empty()); + + res = cpr::Post( + cpr::Url{thing_base_url + "/actions"}, + cpr::Body{json{{"test-action", {{"input", 42}}}}.dump()} + ); + REQUIRE(res.status_code == 201); + auto a1 = json::parse(res.text); + REQUIRE(a1["test-action"].contains("href")); + + std::string a1_href = a1["test-action"]["href"]; + res = cpr::Get(cpr::Url{base_url + a1_href}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text)["test-action"]["input"] == 42); + + res = cpr::Get(cpr::Url{thing_base_url + "/actions"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text).size() == 1); + + res = cpr::Post( + cpr::Url{thing_base_url + "/actions"}, + cpr::Body{json{{"test-action", {{"input", 123}}}}.dump()} + ); + REQUIRE(res.status_code == 201); + auto a2 = json::parse(res.text); + REQUIRE(a2["test-action"].contains("href")); + + res = cpr::Get(cpr::Url{thing_base_url + "/actions/test-action"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text).size() == 2); + + res = cpr::Put(cpr::Url{base_url + a1_href}); + REQUIRE(res.status_code == 200); + + res = cpr::Delete(cpr::Url{base_url + a1_href}); + REQUIRE(res.status_code == 204); // no content + + // action already deleted + res = cpr::Get(cpr::Url{base_url + a1_href}); + REQUIRE(res.status_code == 404); // not found + + res = cpr::Get(cpr::Url{thing_base_url + "/actions"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text).size() == 1); + REQUIRE(json::parse(res.text)[0]["test-action"]["input"] == 123); + + res = cpr::Delete(cpr::Url{base_url + a1_href}); + REQUIRE(res.status_code == 404); // not found + + res = cpr::Get(cpr::Url{thing_base_url + "/actions/not-existing-action/123-456"}); + REQUIRE(res.status_code == 404); // not found + + res = cpr::Delete(cpr::Url{thing_base_url + "/actions/not-existing-action/123-456"}); + REQUIRE(res.status_code == 404); // not found + + res = cpr::Post( + cpr::Url{thing_base_url + "/actions"} /*no body*/ + ); + REQUIRE(res.status_code == 400); // bad request + + res = cpr::Post( + cpr::Url{thing_base_url + "/actions/test-action"}, + cpr::Body{json{{"invalid-action-body", {{"foo", "bar"}}}}.dump()} + ); + REQUIRE(res.status_code == 400); // bad request + + res = cpr::Post( + cpr::Url{thing_base_url + "/actions/throwing-test-action"}, + cpr::Body{json{{"throwing-test-action", {{"input", "some-string-but-number-expected"}}}}.dump()} + ); + REQUIRE(res.status_code == 400); // bad request + + // thing not existing + res = cpr::Get(cpr::Url{base_url + "/42/actions/test-action"}); + REQUIRE(res.status_code == 404); // not found + + res = cpr::Get(cpr::Url{base_url + "/42/actions/test-action/123-456"}); + REQUIRE(res.status_code == 404); // not found + + res = cpr::Delete(cpr::Url{base_url + "/42/actions/test-action/123-456"}); + REQUIRE(res.status_code == 404); // not found + + res = cpr::Put(cpr::Url{base_url + "/42/actions/test-action/123-456"}); + REQUIRE(res.status_code == 404); // not found + + res = cpr::Post( + cpr::Url{base_url + "/42/actions/test-action"} /*no body*/ + ); + REQUIRE(res.status_code == 404); // not found + }); +} + +TEST_CASE( "It offers REST api for events", "[server]" ) +{ + auto thing = make_thing("uri:test:1", "single-thing"); + link_event(thing, "count-event", {{"title", "Count Event"}, {"type", "number"}}); + link_event(thing, "message-event", {{"title", "Message Event"}, {"type", "string"}}); + + auto thing_container = MultipleThings({thing.get()}, "single-thing-in-multi-container"); + auto builder = WebThingServer::host(thing_container).port(57888); + + test_running_server(builder, [&thing](WebThingServer* server, const std::string& base_url) + { + std::string thing_base_url = base_url + "/0"; + + REQUIRE(server->get_name() == "single-thing-in-multi-container"); + auto res = cpr::Get(cpr::Url{thing_base_url}); + REQUIRE(res.status_code == 200); + auto td = json::parse(res.text); + REQUIRE(td["events"]["count-event"]["title"] == "Count Event"); + REQUIRE(td["events"]["message-event"]["title"] == "Message Event"); + + res = cpr::Get(cpr::Url{thing_base_url + "/events"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text).empty()); + + res = cpr::Get(cpr::Url{thing_base_url + "/events/count-event"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text).empty()); + + emit_event(thing, "count-event", 1); + res = cpr::Get(cpr::Url{thing_base_url + "/events/count-event"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text).size() == 1); + REQUIRE(json::parse(res.text)[0]["count-event"]["data"] == 1); + + emit_event(thing, "message-event", "msg-a"); + res = cpr::Get(cpr::Url{thing_base_url + "/events/message-event"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text).size() == 1); + REQUIRE(json::parse(res.text)[0]["message-event"]["data"] == "msg-a"); + + emit_event(thing, "count-event", 2); + res = cpr::Get(cpr::Url{thing_base_url + "/events/count-event"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text).size() == 2); + REQUIRE(json::parse(res.text)[1]["count-event"]["data"] == 2); + + res = cpr::Get(cpr::Url{thing_base_url + "/events"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text).size() == 3); + REQUIRE(json::parse(res.text)[0]["count-event"]["data"] == 1); + REQUIRE(json::parse(res.text)[1]["message-event"]["data"] == "msg-a"); + REQUIRE(json::parse(res.text)[2]["count-event"]["data"] == 2); + + // thing not existing + res = cpr::Get(cpr::Url{base_url + "/42/events"}); + REQUIRE(res.status_code == 404); // not found + + res = cpr::Get(cpr::Url{base_url + "/42/events/test-event"}); + REQUIRE(res.status_code == 404); // not found + }); +} + +TEST_CASE( "It supports custom html ui page", "[server]" ) +{ + auto thing = make_thing("uri:test:1", "single-thing"); + REQUIRE_FALSE(thing->get_ui_href()); + + thing->set_ui_href("/gui.html"); + REQUIRE(thing->get_ui_href() == "/gui.html"); + + + auto thing_container = MultipleThings({thing.get()}, "single-thing-in-multi-container"); + auto builder = WebThingServer::host(thing_container).port(57999); + + test_running_server(builder, [&thing](WebThingServer* server, const std::string& base_url) + { + auto web = server->get_web_server(); + + // register additional html page + web->get("/gui.html", [&](auto res, auto req){ + WebThingServer::Response(req, res).html("

It works...

").end(); + }); + + std::string thing_base_url = base_url + "/0"; + + REQUIRE(server->get_name() == "single-thing-in-multi-container"); + auto res = cpr::Get(cpr::Url{thing_base_url}); + REQUIRE(res.status_code == 200); + auto td = json::parse(res.text); + auto links = td["links"]; + + auto found_gui_link_obj = std::find_if(links.begin(), links.end(), [](const json& link) { + return link.contains("rel") && link["rel"] == "alternate" && + link.contains("mediaType") && link["mediaType"] == "text/html"; + }); + + REQUIRE(found_gui_link_obj != links.end()); + + std::string ui_href = (*found_gui_link_obj)["href"]; + REQUIRE(ui_href == "/gui.html"); + + res = cpr::Get(cpr::Url{base_url + ui_href}); + REQUIRE(res.status_code == 200); + REQUIRE_THAT(res.header["Content-Type"], Catch::Matchers::StartsWith("text/html")); + REQUIRE(res.text == "

It works...

"); + }); +} diff --git a/test/catch2/unit-tests/thing_tests.cpp b/test/catch2/unit-tests/thing_tests.cpp index f0eaa1c..98643f8 100644 --- a/test/catch2/unit-tests/thing_tests.cpp +++ b/test/catch2/unit-tests/thing_tests.cpp @@ -127,6 +127,12 @@ TEST_CASE( "Webthing things performes actions", "[action][thing]" ) test_thing->remove_action("my-custom-action", "abc-123"); REQUIRE_FALSE( test_thing->get_action("my-custom-action", "abc-123") ); + + // try to perform unavailable action + { + FIXED_UUID_SCOPED("ghi-789"); + REQUIRE_FALSE( test_thing->perform_action("some-unavailable-action", "my-input-string") ); + } } #ifdef WT_USE_JSON_SCHEMA_VALIDATION diff --git a/vcpkg-no-ssl.json b/vcpkg-no-ssl.json index 0506a2e..d017ace 100644 --- a/vcpkg-no-ssl.json +++ b/vcpkg-no-ssl.json @@ -4,6 +4,7 @@ "builtin-baseline": "d5ec528843d29e3a52d745a64b469f810b2cedbf", "dependencies": [ "catch2", + "cpr", "json-schema-validator", "mdns", "nlohmann-json", diff --git a/vcpkg-with-ssl.json b/vcpkg-with-ssl.json index b9f6386..8f7c7bd 100644 --- a/vcpkg-with-ssl.json +++ b/vcpkg-with-ssl.json @@ -4,6 +4,7 @@ "builtin-baseline": "d5ec528843d29e3a52d745a64b469f810b2cedbf", "dependencies": [ "catch2", + "cpr", "json-schema-validator", "mdns", "nlohmann-json", diff --git a/vcpkg.json b/vcpkg.json index 0506a2e..d017ace 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -4,6 +4,7 @@ "builtin-baseline": "d5ec528843d29e3a52d745a64b469f810b2cedbf", "dependencies": [ "catch2", + "cpr", "json-schema-validator", "mdns", "nlohmann-json", From 4007b0d30ce085bd64bbcb000fd5ac7d9f7144e1 Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Wed, 5 Mar 2025 06:56:00 +0100 Subject: [PATCH 05/12] run tests detailed for macos --- .github/workflows/macos.yml | 6 +++++- build.sh | 25 ++++++++++++++++++++++++- test/CMakeLists.txt | 10 +++++++--- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 8e49960..13eaf9d 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -22,12 +22,16 @@ jobs: - name: Build run: | chmod +x ./build.sh - ./build.sh clean release + ./build.sh clean release skip_tests chmod +x ./build/examples/single-thing chmod +x ./build/examples/multiple-things chmod +x ./build/examples/gui-thing chmod +x ./build/test/tests + - name: Run tests with -s flag + run: | + ./build/test/tests -s + - name: Integration tests working-directory: ${{github.workspace}}/test/webthing-tester run: | diff --git a/build.sh b/build.sh index 134d150..5235f11 100644 --- a/build.sh +++ b/build.sh @@ -36,8 +36,31 @@ fi echo "project SSL support: $ssl_support" cp $vcpkg_file vcpkg.json +if [[ "${@#without_tests}" = "$@" ]] +then + build_tests="ON" +else + build_tests="OFF" +fi +echo "project build tests: $build_tests" + +if [[ "${@#skip_tests}" = "$@" ]] +then + skip_tests="OFF" +else + skip_tests="ON" +fi +echo "project skip tests: $build_tests" + +if [[ "${@#without_examples}" = "$@" ]] +then + build_examples="ON" +else + build_examples="OFF" +fi +echo "project build examples: $build_examples" -cmake -B build -S . -D"WT_WITH_SSL=$ssl_support" -D"CMAKE_BUILD_TYPE=$build_type" -D"WT_ENABLE_COVERAGE=$code_coverage" -D"CMAKE_TOOLCHAIN_FILE=$toolchain_file" -D"CMAKE_MAKE_PROGRAM:PATH=make" -D"CMAKE_CXX_COMPILER=g++" +cmake -B build -S . -D"WT_BUILD_TESTS=$build_tests" -D"WT_SKIP_TESTS=$skip_tests" -D"WT_BUILD_EXAMPLES=$build_examples" -D"WT_WITH_SSL=$ssl_support" -D"CMAKE_BUILD_TYPE=$build_type" -D"WT_ENABLE_COVERAGE=$code_coverage" -D"CMAKE_TOOLCHAIN_FILE=$toolchain_file" -D"CMAKE_MAKE_PROGRAM:PATH=make" -D"CMAKE_CXX_COMPILER=g++" cmake --build build --parallel $(nproc) ctest --test-dir build/test/ \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9cb0298..313ba90 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -35,6 +35,10 @@ if(WT_ENABLE_COVERAGE) endif() endif() -include(CTest) -include(Catch) -catch_discover_tests(tests) \ No newline at end of file +option(WT_SKIP_TESTS "skip tests execution" OFF) +message("WT_SKIP_TESTS: ${WT_SKIP_TESTS}") +if(NOT WT_SKIP_TESTS) + include(CTest) + include(Catch) + catch_discover_tests(tests) +endif() \ No newline at end of file From 04bd1a9b0ae6f87db50e5e62e659c2a59cfd8828 Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Wed, 5 Mar 2025 07:24:47 +0100 Subject: [PATCH 06/12] remove -s flag for macos tests --- .github/workflows/macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 13eaf9d..3b4e4ed 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -30,7 +30,7 @@ jobs: - name: Run tests with -s flag run: | - ./build/test/tests -s + ./build/test/tests - name: Integration tests working-directory: ${{github.workspace}}/test/webthing-tester From 431228e859395f544af4d42d5fd26d3524cf8d00 Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Wed, 5 Mar 2025 18:33:30 +0100 Subject: [PATCH 07/12] try macos test via cmake again --- .github/workflows/macos.yml | 10 +++------- test/catch2/unit-tests/action_tests.cpp | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 3b4e4ed..0c63345 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -10,7 +10,7 @@ on: jobs: build-and-test: runs-on: macos-latest - + steps: - uses: actions/checkout@v4 @@ -18,20 +18,16 @@ jobs: run: | chmod +x ./tools/install-vcpkg.sh ./tools/install-vcpkg.sh - + - name: Build run: | chmod +x ./build.sh - ./build.sh clean release skip_tests + ./build.sh clean release chmod +x ./build/examples/single-thing chmod +x ./build/examples/multiple-things chmod +x ./build/examples/gui-thing chmod +x ./build/test/tests - - name: Run tests with -s flag - run: | - ./build/test/tests - - name: Integration tests working-directory: ${{github.workspace}}/test/webthing-tester run: | diff --git a/test/catch2/unit-tests/action_tests.cpp b/test/catch2/unit-tests/action_tests.cpp index 2314af4..48eb65a 100644 --- a/test/catch2/unit-tests/action_tests.cpp +++ b/test/catch2/unit-tests/action_tests.cpp @@ -69,7 +69,7 @@ SCENARIO( "actions have a stateful lifecycle", "[action]" ) logger::info("CustomAction perform action with input:" + get_input().value_or(json()).dump()); auto start_time = std::chrono::steady_clock::now(); - auto max_duration = std::chrono::milliseconds(200); + auto max_duration = std::chrono::milliseconds(300); while (!cancel_work && std::chrono::steady_clock::now() - start_time < max_duration) { From 25f5aad4cef5febfdbfb2905378f106903d7f9a4 Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Thu, 6 Mar 2025 02:50:31 +0100 Subject: [PATCH 08/12] add websocket tests --- build.bat | 27 +- test/CMakeLists.txt | 4 +- ...server_tests.cpp => server_http_tests.cpp} | 22 +- test/catch2/unit-tests/server_ws_tests.cpp | 407 ++++++++++++++++++ test/coverage.sh | 4 +- vcpkg-no-ssl.json | 3 +- vcpkg-with-ssl.json | 3 +- vcpkg.json | 3 +- 8 files changed, 454 insertions(+), 19 deletions(-) rename test/catch2/unit-tests/{server_tests.cpp => server_http_tests.cpp} (96%) create mode 100644 test/catch2/unit-tests/server_ws_tests.cpp diff --git a/build.bat b/build.bat index 1711200..3fc464c 100644 --- a/build.bat +++ b/build.bat @@ -31,7 +31,6 @@ if %errorlevel% equ 0 ( ) echo Project architecture: %build_arch% - echo %* | find /i "with_ssl" > nul if %errorlevel% equ 0 ( set "ssl_support=ON" @@ -43,7 +42,31 @@ if %errorlevel% equ 0 ( echo Project SSL support: %ssl_support% copy %vcpkg_file% vcpkg.json -cmake -B "%build_dir%" -S . -DWT_WITH_SSL=%ssl_support% -DCMAKE_BUILD_TYPE=%build_type% -DCMAKE_TOOLCHAIN_FILE="%toolchain_file%" -DVCPKG_TARGET_TRIPLET="%vcpkg_triplet%" -G "Visual Studio 17 2022" -A "%build_arch%" +echo %* | find /i "without_tests" > nul +if %errorlevel% equ 0 ( + set "build_tests=OFF" +) else ( + set "build_tests=ON" +) +echo Project build tests: %build_tests% + +echo %* | find /i "skip_tests" > nul +if %errorlevel% equ 0 ( + set "skip_tests=ON" +) else ( + set "skip_tests=OFF" +) +echo Project skip tests: %skip_tests% + +echo %* | find /i "without_examples" > nul +if %errorlevel% equ 0 ( + set "build_examples=OFF" +) else ( + set "build_examples=ON" +) +echo Project build examples: %build_examples% + +cmake -B "%build_dir%" -S . -DWT_BUILD_TESTS=%build_tests% -DWT_SKIP_TESTS=%skip_tests% -DWT_BUILD_EXAMPLES=%build_examples% -DWT_WITH_SSL=%ssl_support% -DCMAKE_BUILD_TYPE=%build_type% -DCMAKE_TOOLCHAIN_FILE="%toolchain_file%" -DVCPKG_TARGET_TRIPLET="%vcpkg_triplet%" -G "Visual Studio 17 2022" -A "%build_arch%" cmake --build "%build_dir%" --config "%build_type%" --parallel %NUMBER_OF_PROCESSORS% ctest --test-dir "%build_dir%\test\" \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 313ba90..aa64b7a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,12 +1,14 @@ find_package(Catch2 3 REQUIRED) find_package(cpr CONFIG REQUIRED) +find_package(websocketpp CONFIG REQUIRED) add_executable(tests "catch2/unit-tests/action_tests.cpp" "catch2/unit-tests/event_tests.cpp" "catch2/unit-tests/json_validator_tests.cpp" "catch2/unit-tests/property_tests.cpp" - "catch2/unit-tests/server_tests.cpp" + "catch2/unit-tests/server_http_tests.cpp" + "catch2/unit-tests/server_ws_tests.cpp" "catch2/unit-tests/storage_tests.cpp" "catch2/unit-tests/thing_tests.cpp" "catch2/unit-tests/utils_tests.cpp" diff --git a/test/catch2/unit-tests/server_tests.cpp b/test/catch2/unit-tests/server_http_tests.cpp similarity index 96% rename from test/catch2/unit-tests/server_tests.cpp rename to test/catch2/unit-tests/server_http_tests.cpp index 9304c62..7832196 100644 --- a/test/catch2/unit-tests/server_tests.cpp +++ b/test/catch2/unit-tests/server_http_tests.cpp @@ -9,7 +9,7 @@ using namespace bw::webthing; -void test_running_server(WebThingServer::Builder& builder, std::function test_callback) +inline void test_running_server(WebThingServer::Builder& builder, std::function test_callback) { logger::set_level(log_level::trace); auto server = builder.build(); @@ -40,7 +40,7 @@ void test_running_server(WebThingServer::Builder& builder, std::functionget_ui_href()); diff --git a/test/catch2/unit-tests/server_ws_tests.cpp b/test/catch2/unit-tests/server_ws_tests.cpp new file mode 100644 index 0000000..469ee32 --- /dev/null +++ b/test/catch2/unit-tests/server_ws_tests.cpp @@ -0,0 +1,407 @@ +// Webthing-CPP +// SPDX-FileCopyrightText: 2023-present Benno Waldhauer +// SPDX-License-Identifier: MIT + +#include +#include +#include + +#include +#include + +using namespace bw::webthing; + +inline void test_running_server(WebThingServer::Builder& builder, std::function test_callback) +{ + logger::set_level(log_level::trace); + auto server = builder.build(); + REQUIRE(server.get_web_server() != nullptr); + + int port = server.get_port(); + std::string base_path = server.get_base_path(); + std::exception_ptr thread_exception = nullptr; + + auto t = std::thread([&](){ + + try + { + std::string base_url = "http://localhost:" + std::to_string(port) + base_path; + test_callback(&server, base_url); + } catch (...) { + thread_exception = std::current_exception(); + } + + server.stop(); + }); + + server.start(); + t.join(); + + if (thread_exception) { + std::rethrow_exception(thread_exception); + } +}; + +using websocketpp::connection_hdl; +using client = websocketpp::client; + +void connect_via_ws(const std::string& url, std::function*)> client_callback) +{ + std::vector messages_received; + auto on_message = [&](connection_hdl, client::message_ptr msg) + { + auto message = msg->get_payload(); + logger::info("WS_CLIENT RECEIVED: " + message); + messages_received.push_back(json::parse(message)); + }; + + client ws_client; + ws_client.clear_access_channels(websocketpp::log::alevel::all); + ws_client.clear_error_channels(websocketpp::log::elevel::all); + ws_client.init_asio(); + ws_client.set_message_handler(on_message); + + websocketpp::lib::error_code ec; + client::connection_ptr con = ws_client.get_connection(url, ec); + REQUIRE(!ec); // Ensure connection is successful + + ws_client.connect(con); + std::thread client_thread([&]() { ws_client.run(); }); + + std::exception_ptr thread_exception = nullptr; + + try + { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + client_callback(con, &messages_received); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } catch (...) { + thread_exception = std::current_exception(); + } + + con->close(websocketpp::close::status::normal, "Test finished"); + client_thread.join(); + + if (thread_exception) { + std::rethrow_exception(thread_exception); + } +} + +TEST_CASE( "It can make a single thing via websocket available", "[server][ws]" ) +{ + auto thing = make_thing("uri:test:1", "single-thing"); + link_property(thing, "brightness", 50, { + {"@type", "BrightnessProperty"}, + {"title", "Brightness"}, + {"type", "integer"}, + {"description", "The level of light from 0-100"}, + {"minimum", 0}, + {"maximum", 100}, + {"unit", "percent"}}); + + auto thing_container = SingleThing(thing.get()); + auto builder = WebThingServer::host(thing_container).port(57111); + + test_running_server(builder, [](WebThingServer* server, const std::string& base_url) + { + REQUIRE(server->get_name() == "single-thing"); + auto res = cpr::Get(cpr::Url{base_url}); + REQUIRE(res.status_code == 200); + auto td = json::parse(res.text); + REQUIRE(td["title"] == "single-thing"); + + auto links = td["links"]; + auto found_ws_link_obj = std::find_if(links.begin(), links.end(), [](const json& link) { + return link.contains("rel") && link["rel"] == "alternate" && !link.contains("mediaType"); + }); + REQUIRE(found_ws_link_obj != links.end()); + std::string ws_url = (*found_ws_link_obj)["href"]; + + connect_via_ws(ws_url, [&](client::connection_ptr con, std::vector* received_messages_ptr) + { + std::vector& received_messages = *received_messages_ptr; + + res = cpr::Put( + cpr::Url{base_url + "/properties/brightness"}, + cpr::Body{json{{"brightness", 42}}.dump()} + ); + REQUIRE(res.status_code == 200); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(received_messages.back()["messageType"] == "propertyStatus"); + REQUIRE(received_messages.back()["data"]["brightness"] == 42); + + con->send(json{{"messageType", "setProperty"}, {"data", {{"brightness", 24}}}}.dump()); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(received_messages.back()["messageType"] == "propertyStatus"); + REQUIRE(received_messages.back()["data"]["brightness"] == 24); + + // send no json message + con->send("Some string not beeing json..."); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(received_messages.back()["messageType"] == "error"); + REQUIRE(received_messages.back()["data"]["status"] == "400 Bad Request"); + REQUIRE(received_messages.back()["data"]["message"] == "Parsing request failed"); + + // send json message missing messageType + con->send(json{{"data", {{"brightness", 666}}}}.dump()); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(received_messages.back()["messageType"] == "error"); + REQUIRE(received_messages.back()["data"]["status"] == "400 Bad Request"); + REQUIRE(received_messages.back()["data"]["message"] == "Invalid message"); + + // send json message missing data + con->send(json{{"messageType", "setProperty"}}.dump()); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(received_messages.back()["messageType"] == "error"); + REQUIRE(received_messages.back()["data"]["status"] == "400 Bad Request"); + REQUIRE(received_messages.back()["data"]["message"] == "Invalid message"); + + // send json message with wrong data type + con->send(json{{"messageType", "setProperty"}, {"data", {{"brightness", "some-unexpected-string"}}}}.dump()); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(received_messages.back()["messageType"] == "error"); + REQUIRE(received_messages.back()["data"]["status"] == "400 Bad Request"); + REQUIRE(received_messages.back()["data"]["message"] == "Property value type not matching"); + + // send json message with invalid messageType + con->send(json{{"messageType", "invalidCommand"}, {"data", {{"perform", "invalid-task"}}}}.dump()); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(received_messages.back()["messageType"] == "error"); + REQUIRE(received_messages.back()["data"]["status"] == "400 Bad Request"); + REQUIRE(received_messages.back()["data"]["message"] == "Unknown messageType: invalidCommand"); + }); + + res = cpr::Get(cpr::Url{base_url + "/properties"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text)["brightness"] == 24); + }); +} + +TEST_CASE( "It can make multiple things via websocket available", "[server][ws]" ) +{ + auto thing_a = make_thing("uri:test:a", "thing-a"); + + link_property(thing_a, "boolean-prop", true, { + {"title", "Bool Property"}, + {"type", "boolean"} + }); + + link_property(thing_a, "double-prop", 42.13, { + {"title", "Double Property"}, + {"type", "number"} + }); + + link_property(thing_a, "string-prop", std::string("the-value"), { + {"title", "String Property"}, + {"type", "string"} + }); + + auto thing_b = make_thing("uri:test:b", "thing-b"); + + link_property(thing_b, "object-prop", json{{"key", "value"}}, { + {"title", "Object Property"}, + {"type", "object"} + }); + + link_property(thing_b, "array-prop", json{"some", "values", 42}, { + {"title", "Array Property"}, + {"type", "array"} + }); + + auto thing_container = MultipleThings({thing_a.get(), thing_b.get()}, "things-a-and-b"); + + auto builder = WebThingServer::host(thing_container).port(57112); + test_running_server(builder, [](WebThingServer* server, const std::string& base_url) + { + REQUIRE(server->get_name() == "things-a-and-b"); + auto res = cpr::Get(cpr::Url{base_url}); + REQUIRE(res.status_code == 200); + auto td = json::parse(res.text); + + auto links_a = td[0]["links"]; + auto found_ws_link_a_obj = std::find_if(links_a.begin(), links_a.end(), [](const json& link) { + return link.contains("rel") && link["rel"] == "alternate" && !link.contains("mediaType"); + }); + REQUIRE(found_ws_link_a_obj != links_a.end()); + std::string ws_url_a = (*found_ws_link_a_obj)["href"]; + + + auto links_b = td[1]["links"]; + auto found_ws_link_b_obj = std::find_if(links_b.begin(), links_b.end(), [](const json& link) { + return link.contains("rel") && link["rel"] == "alternate" && !link.contains("mediaType"); + }); + REQUIRE(found_ws_link_b_obj != links_b.end()); + std::string ws_url_b = (*found_ws_link_b_obj)["href"]; + + + connect_via_ws(ws_url_a, [&](client::connection_ptr con, std::vector* received_messages_ptr) + { + std::vector& received_messages = *received_messages_ptr; + + con->send(json{{"messageType", "setProperty"}, {"data", {{"boolean-prop", false}}}}.dump()); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(received_messages.back()["messageType"] == "propertyStatus"); + REQUIRE(received_messages.back()["data"]["boolean-prop"] == false); + + con->send(json{{"messageType", "setProperty"}, {"data", {{"double-prop", 24.0}}}}.dump()); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(received_messages.back()["messageType"] == "propertyStatus"); + REQUIRE(received_messages.back()["data"]["double-prop"] == 24.0); + + con->send(json{{"messageType", "setProperty"}, {"data", {{"string-prop", "the-updated-value"}}}}.dump()); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(received_messages.back()["messageType"] == "propertyStatus"); + REQUIRE(received_messages.back()["data"]["string-prop"] == "the-updated-value"); + }); + + res = cpr::Get(cpr::Url{base_url + "/0/properties"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text)["boolean-prop"] == false); + REQUIRE(json::parse(res.text)["double-prop"] == 24.0); + REQUIRE(json::parse(res.text)["string-prop"] == "the-updated-value"); + + connect_via_ws(ws_url_b, [&](client::connection_ptr con, std::vector* received_messages_ptr) + { + std::vector& received_messages = *received_messages_ptr; + + con->send(json{{"messageType", "setProperty"}, {"data", {{"object-prop", {{"key", "updated-value"}}}}}}.dump()); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(received_messages.back()["messageType"] == "propertyStatus"); + REQUIRE(received_messages.back()["data"]["object-prop"] == json{{"key", "updated-value"}}); + + con->send(json{{"messageType", "setProperty"}, {"data", {{"array-prop", {"a", "b", "c", 42}}}}}.dump()); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(received_messages.back()["messageType"] == "propertyStatus"); + REQUIRE(received_messages.back()["data"]["array-prop"] == json{"a", "b", "c", 42}); + }); + + res = cpr::Get(cpr::Url{base_url + "/1/properties"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text)["object-prop"] == json{{"key", "updated-value"}}); + REQUIRE(json::parse(res.text)["array-prop"] == json{"a", "b", "c", 42}); + }); +} + + +TEST_CASE( "It offers websocket api for events", "[server][ws]" ) +{ + auto thing = make_thing("uri:test:1", "single-thing"); + link_event(thing, "count-event", {{"title", "Count Event"}, {"type", "number"}}); + link_event(thing, "message-event", {{"title", "Message Event"}, {"type", "string"}}); + + auto thing_container = SingleThing(thing.get()); + auto builder = WebThingServer::host(thing_container).port(57113); + + test_running_server(builder, [&](WebThingServer* server, const std::string& base_url) + { + REQUIRE(server->get_name() == "single-thing"); + auto res = cpr::Get(cpr::Url{base_url}); + REQUIRE(res.status_code == 200); + auto td = json::parse(res.text); + REQUIRE(td["title"] == "single-thing"); + + res = cpr::Get(cpr::Url{base_url + "/events"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text).empty()); + + auto links = td["links"]; + auto found_ws_link_obj = std::find_if(links.begin(), links.end(), [](const json& link) { + return link.contains("rel") && link["rel"] == "alternate" && !link.contains("mediaType"); + }); + REQUIRE(found_ws_link_obj != links.end()); + std::string ws_url = (*found_ws_link_obj)["href"]; + + connect_via_ws(ws_url, [&](client::connection_ptr con, std::vector* received_messages_ptr) + { + std::vector& received_messages = *received_messages_ptr; + + // event emitted without websocket subscribers + emit_event(thing, "count-event", 0); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(received_messages.empty()); + + // subscribe to events + con->send(json{{"messageType", "addEventSubscription"}, {"data", {{"count-event", json::object()}}}}.dump()); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + con->send(json{{"messageType", "addEventSubscription"}, {"data", {{"message-event", json::object()}}}}.dump()); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + // events emitted with websocket subscribers + emit_event(thing, "count-event", 1); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(received_messages.back()["messageType"] == "event"); + REQUIRE(received_messages.back()["data"]["count-event"]["data"] == 1); + + emit_event(thing, "message-event", "msg-a"); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(received_messages.back()["messageType"] == "event"); + REQUIRE(received_messages.back()["data"]["message-event"]["data"] == "msg-a"); + + emit_event(thing, "count-event", 2); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(received_messages.back()["messageType"] == "event"); + REQUIRE(received_messages.back()["data"]["count-event"]["data"] == 2); + }); + + res = cpr::Get(cpr::Url{base_url + "/events"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text).size() == 4); + }); +} + + +TEST_CASE( "It offers websocket api for actions", "[server][ws]" ) +{ + auto thing = make_thing("uri:test:1", "single-thing"); + + link_action(thing, "test-action", json{{"title", "Test Action"}}, []{ + logger::info("PERFORM TEST ACTION"); + }); + + auto thing_container = SingleThing(thing.get()); + auto builder = WebThingServer::host(thing_container).port(57114); + + test_running_server(builder, [&](WebThingServer* server, const std::string& base_url) + { + REQUIRE(server->get_name() == "single-thing"); + auto res = cpr::Get(cpr::Url{base_url}); + REQUIRE(res.status_code == 200); + auto td = json::parse(res.text); + REQUIRE(td["title"] == "single-thing"); + + res = cpr::Get(cpr::Url{base_url + "/actions"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text).empty()); + + auto links = td["links"]; + auto found_ws_link_obj = std::find_if(links.begin(), links.end(), [](const json& link) { + return link.contains("rel") && link["rel"] == "alternate" && !link.contains("mediaType"); + }); + REQUIRE(found_ws_link_obj != links.end()); + std::string ws_url = (*found_ws_link_obj)["href"]; + + connect_via_ws(ws_url, [&](client::connection_ptr con, std::vector* received_messages_ptr) + { + std::vector& received_messages = *received_messages_ptr; + + con->send(json{{"messageType", "requestAction"}, {"data", {{"test-action", {{"input", 42}}}}}}.dump()); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + REQUIRE(received_messages.size() == 3); + + REQUIRE(received_messages[0]["messageType"] == "actionStatus"); + REQUIRE(received_messages[0]["data"]["test-action"]["status"] == "created"); + + REQUIRE(received_messages[1]["messageType"] == "actionStatus"); + REQUIRE(received_messages[1]["data"]["test-action"]["status"] == "pending"); + + REQUIRE(received_messages[2]["messageType"] == "actionStatus"); + REQUIRE(received_messages[2]["data"]["test-action"]["status"] == "completed"); + }); + + res = cpr::Get(cpr::Url{base_url + "/actions"}); + REQUIRE(res.status_code == 200); + REQUIRE(json::parse(res.text).size() == 1); + }); +} diff --git a/test/coverage.sh b/test/coverage.sh index ab660c8..1d4da14 100644 --- a/test/coverage.sh +++ b/test/coverage.sh @@ -4,7 +4,7 @@ script_path=$(realpath "$0") base_dir="$( dirname "$script_path" )/.." build_dir="$base_dir/build" -${base_dir}/build.sh clean debug +${base_dir}/build.sh clean debug without_examples echo "generate coverage.info:" lcov --capture --directory . --output-file "${build_dir}/coverage.info" @@ -34,7 +34,7 @@ echo 'Webthing-CPP: a modern CPP implementation of th echo '
' >> $html_prolog rm -Rf "${build_dir}/coverage_report" -genhtml "${build_dir}/filtered_coverage.info" -o "${build_dir}/coverage_report" --html-prolog "${build_dir}/coverage_report_prolog.html" --rc genhtml_hi_limit=60 --rc genhtml_med_limit=50 +genhtml "${build_dir}/filtered_coverage.info" -o "${build_dir}/coverage_report" --html-prolog "${build_dir}/coverage_report_prolog.html" --rc genhtml_hi_limit=80 --rc genhtml_med_limit=60 echo "download coverage badge" curl -o "${build_dir}/coverage_report/badge.svg" "https://img.shields.io/badge/coverage-${total_line_coverage}25-rgb%2850,201,85%29" diff --git a/vcpkg-no-ssl.json b/vcpkg-no-ssl.json index d017ace..fd9ce72 100644 --- a/vcpkg-no-ssl.json +++ b/vcpkg-no-ssl.json @@ -8,7 +8,8 @@ "json-schema-validator", "mdns", "nlohmann-json", - "uwebsockets" + "uwebsockets", + "websocketpp" ], "overrides": [ { diff --git a/vcpkg-with-ssl.json b/vcpkg-with-ssl.json index 8f7c7bd..36ad759 100644 --- a/vcpkg-with-ssl.json +++ b/vcpkg-with-ssl.json @@ -13,7 +13,8 @@ "features": [ "ssl" ] - } + }, + "websocketpp" ], "overrides": [ { diff --git a/vcpkg.json b/vcpkg.json index d017ace..fd9ce72 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -8,7 +8,8 @@ "json-schema-validator", "mdns", "nlohmann-json", - "uwebsockets" + "uwebsockets", + "websocketpp" ], "overrides": [ { From 8e0f5279c855e57b1aefdcf0e378c74ae38ab6f9 Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Thu, 6 Mar 2025 22:30:02 +0100 Subject: [PATCH 09/12] replace websocketpp by ixwebsocket for tests --- test/CMakeLists.txt | 3 +- test/catch2/unit-tests/server_ws_tests.cpp | 87 ++++++++++------------ vcpkg-no-ssl.json | 2 +- vcpkg-with-ssl.json | 2 +- vcpkg.json | 2 +- 5 files changed, 45 insertions(+), 51 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index aa64b7a..e73cc62 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,6 +1,6 @@ find_package(Catch2 3 REQUIRED) find_package(cpr CONFIG REQUIRED) -find_package(websocketpp CONFIG REQUIRED) +find_package(ixwebsocket CONFIG REQUIRED) add_executable(tests "catch2/unit-tests/action_tests.cpp" @@ -25,6 +25,7 @@ target_compile_definitions(tests PRIVATE CATCH_CONFIG_ENABLE_ALL_STRINGMAKERS) target_include_directories(tests PRIVATE ${INCLUDES_FOR_TESTS}) target_link_libraries(tests PRIVATE Catch2::Catch2WithMain) target_link_libraries(tests PRIVATE cpr::cpr) +target_link_libraries(tests PRIVATE ixwebsocket::ixwebsocket) target_link_libraries(tests PRIVATE nlohmann_json_schema_validator::validator) target_link_libraries(tests PRIVATE unofficial::uwebsockets::uwebsockets) diff --git a/test/catch2/unit-tests/server_ws_tests.cpp b/test/catch2/unit-tests/server_ws_tests.cpp index 469ee32..3c5426a 100644 --- a/test/catch2/unit-tests/server_ws_tests.cpp +++ b/test/catch2/unit-tests/server_ws_tests.cpp @@ -4,11 +4,9 @@ #include #include +#include #include -#include -#include - using namespace bw::webthing; inline void test_running_server(WebThingServer::Builder& builder, std::function test_callback) @@ -42,47 +40,42 @@ inline void test_running_server(WebThingServer::Builder& builder, std::function< } }; -using websocketpp::connection_hdl; -using client = websocketpp::client; - -void connect_via_ws(const std::string& url, std::function*)> client_callback) +void connect_via_ws(const std::string& url, std::function*)> client_callback) { std::vector messages_received; - auto on_message = [&](connection_hdl, client::message_ptr msg) - { - auto message = msg->get_payload(); - logger::info("WS_CLIENT RECEIVED: " + message); - messages_received.push_back(json::parse(message)); - }; - client ws_client; - ws_client.clear_access_channels(websocketpp::log::alevel::all); - ws_client.clear_error_channels(websocketpp::log::elevel::all); - ws_client.init_asio(); - ws_client.set_message_handler(on_message); + ix::WebSocket ws; + ws.setUrl(url); - websocketpp::lib::error_code ec; - client::connection_ptr con = ws_client.get_connection(url, ec); - REQUIRE(!ec); // Ensure connection is successful + ws.setOnMessageCallback([&](const ix::WebSocketMessagePtr& msg) + { + if (msg->type == ix::WebSocketMessageType::Message) + { + std::string message = msg->str; + logger::info("WS_CLIENT RECEIVED: " + message); + messages_received.push_back(json::parse(message)); + } + }); - ws_client.connect(con); - std::thread client_thread([&]() { ws_client.run(); }); + ws.start(); std::exception_ptr thread_exception = nullptr; try { std::this_thread::sleep_for(std::chrono::milliseconds(10)); - client_callback(con, &messages_received); + client_callback(&ws, &messages_received); std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } catch (...) { + } + catch (...) + { thread_exception = std::current_exception(); } - con->close(websocketpp::close::status::normal, "Test finished"); - client_thread.join(); + ws.stop(); - if (thread_exception) { + if (thread_exception) + { std::rethrow_exception(thread_exception); } } @@ -117,7 +110,7 @@ TEST_CASE( "It can make a single thing via websocket available", "[server][ws]" REQUIRE(found_ws_link_obj != links.end()); std::string ws_url = (*found_ws_link_obj)["href"]; - connect_via_ws(ws_url, [&](client::connection_ptr con, std::vector* received_messages_ptr) + connect_via_ws(ws_url, [&](auto con, std::vector* received_messages_ptr) { std::vector& received_messages = *received_messages_ptr; @@ -130,41 +123,41 @@ TEST_CASE( "It can make a single thing via websocket available", "[server][ws]" REQUIRE(received_messages.back()["messageType"] == "propertyStatus"); REQUIRE(received_messages.back()["data"]["brightness"] == 42); - con->send(json{{"messageType", "setProperty"}, {"data", {{"brightness", 24}}}}.dump()); + con->sendText(json{{"messageType", "setProperty"}, {"data", {{"brightness", 24}}}}.dump()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); REQUIRE(received_messages.back()["messageType"] == "propertyStatus"); REQUIRE(received_messages.back()["data"]["brightness"] == 24); // send no json message - con->send("Some string not beeing json..."); + con->sendText("Some string not beeing json..."); std::this_thread::sleep_for(std::chrono::milliseconds(10)); REQUIRE(received_messages.back()["messageType"] == "error"); REQUIRE(received_messages.back()["data"]["status"] == "400 Bad Request"); REQUIRE(received_messages.back()["data"]["message"] == "Parsing request failed"); // send json message missing messageType - con->send(json{{"data", {{"brightness", 666}}}}.dump()); + con->sendText(json{{"data", {{"brightness", 666}}}}.dump()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); REQUIRE(received_messages.back()["messageType"] == "error"); REQUIRE(received_messages.back()["data"]["status"] == "400 Bad Request"); REQUIRE(received_messages.back()["data"]["message"] == "Invalid message"); // send json message missing data - con->send(json{{"messageType", "setProperty"}}.dump()); + con->sendText(json{{"messageType", "setProperty"}}.dump()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); REQUIRE(received_messages.back()["messageType"] == "error"); REQUIRE(received_messages.back()["data"]["status"] == "400 Bad Request"); REQUIRE(received_messages.back()["data"]["message"] == "Invalid message"); // send json message with wrong data type - con->send(json{{"messageType", "setProperty"}, {"data", {{"brightness", "some-unexpected-string"}}}}.dump()); + con->sendText(json{{"messageType", "setProperty"}, {"data", {{"brightness", "some-unexpected-string"}}}}.dump()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); REQUIRE(received_messages.back()["messageType"] == "error"); REQUIRE(received_messages.back()["data"]["status"] == "400 Bad Request"); REQUIRE(received_messages.back()["data"]["message"] == "Property value type not matching"); // send json message with invalid messageType - con->send(json{{"messageType", "invalidCommand"}, {"data", {{"perform", "invalid-task"}}}}.dump()); + con->sendText(json{{"messageType", "invalidCommand"}, {"data", {{"perform", "invalid-task"}}}}.dump()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); REQUIRE(received_messages.back()["messageType"] == "error"); REQUIRE(received_messages.back()["data"]["status"] == "400 Bad Request"); @@ -234,21 +227,21 @@ TEST_CASE( "It can make multiple things via websocket available", "[server][ws]" std::string ws_url_b = (*found_ws_link_b_obj)["href"]; - connect_via_ws(ws_url_a, [&](client::connection_ptr con, std::vector* received_messages_ptr) + connect_via_ws(ws_url_a, [&](auto con, std::vector* received_messages_ptr) { std::vector& received_messages = *received_messages_ptr; - con->send(json{{"messageType", "setProperty"}, {"data", {{"boolean-prop", false}}}}.dump()); + con->sendText(json{{"messageType", "setProperty"}, {"data", {{"boolean-prop", false}}}}.dump()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); REQUIRE(received_messages.back()["messageType"] == "propertyStatus"); REQUIRE(received_messages.back()["data"]["boolean-prop"] == false); - con->send(json{{"messageType", "setProperty"}, {"data", {{"double-prop", 24.0}}}}.dump()); + con->sendText(json{{"messageType", "setProperty"}, {"data", {{"double-prop", 24.0}}}}.dump()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); REQUIRE(received_messages.back()["messageType"] == "propertyStatus"); REQUIRE(received_messages.back()["data"]["double-prop"] == 24.0); - con->send(json{{"messageType", "setProperty"}, {"data", {{"string-prop", "the-updated-value"}}}}.dump()); + con->sendText(json{{"messageType", "setProperty"}, {"data", {{"string-prop", "the-updated-value"}}}}.dump()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); REQUIRE(received_messages.back()["messageType"] == "propertyStatus"); REQUIRE(received_messages.back()["data"]["string-prop"] == "the-updated-value"); @@ -260,16 +253,16 @@ TEST_CASE( "It can make multiple things via websocket available", "[server][ws]" REQUIRE(json::parse(res.text)["double-prop"] == 24.0); REQUIRE(json::parse(res.text)["string-prop"] == "the-updated-value"); - connect_via_ws(ws_url_b, [&](client::connection_ptr con, std::vector* received_messages_ptr) + connect_via_ws(ws_url_b, [&](auto con, std::vector* received_messages_ptr) { std::vector& received_messages = *received_messages_ptr; - con->send(json{{"messageType", "setProperty"}, {"data", {{"object-prop", {{"key", "updated-value"}}}}}}.dump()); + con->sendText(json{{"messageType", "setProperty"}, {"data", {{"object-prop", {{"key", "updated-value"}}}}}}.dump()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); REQUIRE(received_messages.back()["messageType"] == "propertyStatus"); REQUIRE(received_messages.back()["data"]["object-prop"] == json{{"key", "updated-value"}}); - con->send(json{{"messageType", "setProperty"}, {"data", {{"array-prop", {"a", "b", "c", 42}}}}}.dump()); + con->sendText(json{{"messageType", "setProperty"}, {"data", {{"array-prop", {"a", "b", "c", 42}}}}}.dump()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); REQUIRE(received_messages.back()["messageType"] == "propertyStatus"); REQUIRE(received_messages.back()["data"]["array-prop"] == json{"a", "b", "c", 42}); @@ -311,7 +304,7 @@ TEST_CASE( "It offers websocket api for events", "[server][ws]" ) REQUIRE(found_ws_link_obj != links.end()); std::string ws_url = (*found_ws_link_obj)["href"]; - connect_via_ws(ws_url, [&](client::connection_ptr con, std::vector* received_messages_ptr) + connect_via_ws(ws_url, [&](auto con, std::vector* received_messages_ptr) { std::vector& received_messages = *received_messages_ptr; @@ -321,10 +314,10 @@ TEST_CASE( "It offers websocket api for events", "[server][ws]" ) REQUIRE(received_messages.empty()); // subscribe to events - con->send(json{{"messageType", "addEventSubscription"}, {"data", {{"count-event", json::object()}}}}.dump()); + con->sendText(json{{"messageType", "addEventSubscription"}, {"data", {{"count-event", json::object()}}}}.dump()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); - con->send(json{{"messageType", "addEventSubscription"}, {"data", {{"message-event", json::object()}}}}.dump()); + con->sendText(json{{"messageType", "addEventSubscription"}, {"data", {{"message-event", json::object()}}}}.dump()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); // events emitted with websocket subscribers @@ -381,11 +374,11 @@ TEST_CASE( "It offers websocket api for actions", "[server][ws]" ) REQUIRE(found_ws_link_obj != links.end()); std::string ws_url = (*found_ws_link_obj)["href"]; - connect_via_ws(ws_url, [&](client::connection_ptr con, std::vector* received_messages_ptr) + connect_via_ws(ws_url, [&](auto con, std::vector* received_messages_ptr) { std::vector& received_messages = *received_messages_ptr; - con->send(json{{"messageType", "requestAction"}, {"data", {{"test-action", {{"input", 42}}}}}}.dump()); + con->sendText(json{{"messageType", "requestAction"}, {"data", {{"test-action", {{"input", 42}}}}}}.dump()); std::this_thread::sleep_for(std::chrono::milliseconds(100)); REQUIRE(received_messages.size() == 3); diff --git a/vcpkg-no-ssl.json b/vcpkg-no-ssl.json index fd9ce72..19cf706 100644 --- a/vcpkg-no-ssl.json +++ b/vcpkg-no-ssl.json @@ -9,7 +9,7 @@ "mdns", "nlohmann-json", "uwebsockets", - "websocketpp" + "ixwebsocket" ], "overrides": [ { diff --git a/vcpkg-with-ssl.json b/vcpkg-with-ssl.json index 36ad759..8baafe3 100644 --- a/vcpkg-with-ssl.json +++ b/vcpkg-with-ssl.json @@ -14,7 +14,7 @@ "ssl" ] }, - "websocketpp" + "ixwebsocket" ], "overrides": [ { diff --git a/vcpkg.json b/vcpkg.json index fd9ce72..19cf706 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -9,7 +9,7 @@ "mdns", "nlohmann-json", "uwebsockets", - "websocketpp" + "ixwebsocket" ], "overrides": [ { From 98bd56321b4ca763ef41794b955315fe09720853 Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Fri, 7 Mar 2025 00:30:00 +0100 Subject: [PATCH 10/12] increase delay for ws client (for win) --- test/catch2/unit-tests/server_ws_tests.cpp | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/test/catch2/unit-tests/server_ws_tests.cpp b/test/catch2/unit-tests/server_ws_tests.cpp index 3c5426a..39aa581 100644 --- a/test/catch2/unit-tests/server_ws_tests.cpp +++ b/test/catch2/unit-tests/server_ws_tests.cpp @@ -46,7 +46,6 @@ void connect_via_ws(const std::string& url, std::functiontype == ix::WebSocketMessageType::Message) @@ -59,25 +58,11 @@ void connect_via_ws(const std::string& url, std::function Date: Fri, 7 Mar 2025 01:40:47 +0100 Subject: [PATCH 11/12] add code coverage action --- .github/workflows/coverage.yml | 52 ++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..beeb3b2 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,52 @@ +name: Code Coverage + +permissions: + pull-requests: write + contents: write + +on: + push: + branches: + - master + pull_request: + +jobs: + coverage_report: + name: Generate coverage report + runs-on: ubuntu-latest + steps: + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup vcpkg + run: | + chmod +x ./tools/install-vcpkg.sh + ./tools/install-vcpkg.sh + + - name: Setup lcov + uses: hrishikesh-kadam/setup-lcov@v1 + + - name: Generate lcov input + run: | + chmod +x ./build.sh + chmod +x ./test/coverage.sh + ./test/coverage.sh + + - name: Report code coverage + if: ${{ github.event_name == 'pull_request' }} + uses: zgosalvez/github-actions-report-lcov@v3 + with: + coverage-files: build/filtered_coverage.info + minimum-coverage: 80 + artifact-name: code-coverage-report + github-token: ${{ secrets.GITHUB_TOKEN }} + update-comment: true + + - name: Upload code coverage report + if: ${{ github.ref_name == 'master' && github.event_name == 'push' }} + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: build/coverage_report + target-folder: coverage-report + branch: gh-pages From 5ea0f39d3baf85f50aa24e311c3ef568867c8e0e Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Fri, 7 Mar 2025 01:52:45 +0100 Subject: [PATCH 12/12] increase version --- include/bw/webthing/version.hpp | 2 +- vcpkg-no-ssl.json | 2 +- vcpkg-with-ssl.json | 2 +- vcpkg.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/bw/webthing/version.hpp b/include/bw/webthing/version.hpp index 9ce0c39..ef8c565 100644 --- a/include/bw/webthing/version.hpp +++ b/include/bw/webthing/version.hpp @@ -8,6 +8,6 @@ namespace bw::webthing { -constexpr const char version[] = "1.1.0"; +constexpr const char version[] = "1.2.0"; } // bw::webthing diff --git a/vcpkg-no-ssl.json b/vcpkg-no-ssl.json index 19cf706..cd5e26d 100644 --- a/vcpkg-no-ssl.json +++ b/vcpkg-no-ssl.json @@ -1,6 +1,6 @@ { "name": "webthing-cpp", - "version-string": "1.1.0", + "version-string": "1.2.0", "builtin-baseline": "d5ec528843d29e3a52d745a64b469f810b2cedbf", "dependencies": [ "catch2", diff --git a/vcpkg-with-ssl.json b/vcpkg-with-ssl.json index 8baafe3..74d5cd3 100644 --- a/vcpkg-with-ssl.json +++ b/vcpkg-with-ssl.json @@ -1,6 +1,6 @@ { "name": "webthing-cpp", - "version-string": "1.1.0", + "version-string": "1.2.0", "builtin-baseline": "d5ec528843d29e3a52d745a64b469f810b2cedbf", "dependencies": [ "catch2", diff --git a/vcpkg.json b/vcpkg.json index 19cf706..cd5e26d 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,6 @@ { "name": "webthing-cpp", - "version-string": "1.1.0", + "version-string": "1.2.0", "builtin-baseline": "d5ec528843d29e3a52d745a64b469f810b2cedbf", "dependencies": [ "catch2",