diff --git a/.github/workflows/cmake-linux.yml b/.github/workflows/cmake-linux.yml index 069cdb1e5..b9bcb7571 100644 --- a/.github/workflows/cmake-linux.yml +++ b/.github/workflows/cmake-linux.yml @@ -55,9 +55,7 @@ jobs: # ipv4 on, tcp on, pki off - args: "-DOC_IPV4_ENABLED=ON -DOC_TCP_ENABLED=ON -DOC_PKI_ENABLED=OFF" # cloud on (ipv4+tcp on), dynamic allocation off, push notifications off - - args: "-DOC_CLOUD_ENABLED=ON -DOC_DYNAMIC_ALLOCATION_ENABLED=OFF -DOC_PUSH_ENABLED=OFF" - # cloud on (ipv4+tcp on), collections create on, oscore off, device provisioning on - - args: "-DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_OSCORE_ENABLED=OFF -DPLGD_DEV_DEVICE_PROVISIONING_ENABLED=ON" + - args: "-DOC_CLOUD_ENABLED=ON -DOC_DYNAMIC_ALLOCATION_ENABLED=OFF -DOC_PUSH_ENABLED=OFF" # cloud on (ipv4+tcp on), collections create on, custom message buffer size, custom message buffer pool size, custom app data buffer size, custom app data buffer pool size - args: "-DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_INOUT_BUFFER_SIZE=2048 -DOC_INOUT_BUFFER_POOL=4 -DOC_APP_DATA_BUFFER_SIZE=2048 -DOC_APP_DATA_BUFFER_POOL=4" # debug on @@ -116,6 +114,8 @@ jobs: # address sanitizer - args: -DOC_ASAN_ENABLED=ON install_faketime: true + - args: -DOC_ASAN_ENABLED=ON -DOC_OSCORE_ENABLED=OFF -DPLGD_DEV_DEVICE_PROVISIONING_ENABLED=ON -DPLGD_DEV_DEVICE_PROVISIONING_TEST_PROPERTIES_ENABLED=ON + install_faketime: true # leak sanitizer - args: -DOC_LSAN_ENABLED=ON # there is some linker issue when LSAN and faketime are used together @@ -125,6 +125,10 @@ jobs: # GCC thread-sanitizer keeps reporting false positives, so we use clang instead for tests with thread-sanitizer clang: true install_faketime: false + - args: -DOC_TSAN_ENABLED=ON -DOC_OSCORE_ENABLED=OFF -DPLGD_DEV_DEVICE_PROVISIONING_ENABLED=ON -DPLGD_DEV_DEVICE_PROVISIONING_TEST_PROPERTIES_ENABLED=ON + # GCC thread-sanitizer keeps reporting false positives, so we use clang instead for tests with thread-sanitizer + clang: true + install_faketime: false # static allocation requires additional thread synchronization - args: -DOC_TSAN_ENABLED=ON -DOC_DYNAMIC_ALLOCATION_ENABLED=OFF # GCC thread-sanitizer keeps reporting false positives, so we use clang instead for tests with thread-sanitizer diff --git a/.github/workflows/sonar-cloud-analysis.yml b/.github/workflows/sonar-cloud-analysis.yml index 864bdb5d0..e27ee950a 100644 --- a/.github/workflows/sonar-cloud-analysis.yml +++ b/.github/workflows/sonar-cloud-analysis.yml @@ -10,11 +10,11 @@ concurrency: cancel-in-progress: ${{ github.ref_name != 'master' }} on: - # push: - # branches: - # - master - # pull_request: - # types: [opened, synchronize, reopened] + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -27,6 +27,8 @@ jobs: include: # cloud (ipv4+tcp) on, collection create on, push on, rfotm on - build_args: "-DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DOC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON" + # cloud on (ipv4+tcp on), collections create on, oscore off, device provisioning on + - args: "-DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_OSCORE_ENABLED=OFF -DPLGD_DEV_DEVICE_PROVISIONING_ENABLED=ON -DPLGD_DEV_DEVICE_PROVISIONING_TEST_PROPERTIES_ENABLED=ON" # security off, ipv4 on, collection create on, push on, max num concurrent requests=1 - build_args: "-DOC_SECURITY_ENABLED=OFF -DOC_IPV4_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DOC_DEVICE_MAX_NUM_CONCURRENT_REQUESTS=1" # ipv6 dns on, oscore off, rep realloc on, json encoder on, introspection IDD off diff --git a/CMakeLists.txt b/CMakeLists.txt index 4456eb505..3e1c94f7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,6 +80,7 @@ set(OC_DEVICE_MAX_NUM_CONCURRENT_REQUESTS "" CACHE STRING "Maximum number of mes # plgd.dev features set(PLGD_DEV_TIME_ENABLED OFF CACHE BOOL "Enable plgd time feature.") set(PLGD_DEV_DEVICE_PROVISIONING_ENABLED OFF CACHE BOOL "Enable plgd's device provisioning feature.") +set(PLGD_DEV_DEVICE_PROVISIONING_TEST_PROPERTIES_ENABLED OFF CACHE BOOL "Enable plgd's device provisioning feature's test properties.") set(OC_ASAN_ENABLED OFF CACHE BOOL "Enable address sanitizer build.") set(OC_LSAN_ENABLED OFF CACHE BOOL "Enable leak sanitizer build.") @@ -427,6 +428,13 @@ if(PLGD_DEV_DEVICE_PROVISIONING_ENABLED) list(APPEND PUBLIC_COMPILE_DEFINITIONS "PLGD_DEV_DEVICE_PROVISIONING") endif() +if(PLGD_DEV_DEVICE_PROVISIONING_TEST_PROPERTIES_ENABLED) + list(APPEND PRIVATE_COMPILE_DEFINITIONS "PLGD_DPS_RESOURCE_TEST_PROPERTIES") + if(BUILD_TESTING) + list(APPEND TEST_COMPILE_DEFINITIONS "PLGD_DPS_RESOURCE_TEST_PROPERTIES") + endif() +endif() + if(BUILD_TESTING) list(APPEND PRIVATE_COMPILE_DEFINITIONS "OC_TEST") if(BUILD_MBEDTLS) diff --git a/api/plgd/device-provisioning-client/plgd_dps_context.c b/api/plgd/device-provisioning-client/plgd_dps_context.c index bf913d228..408b374e7 100644 --- a/api/plgd/device-provisioning-client/plgd_dps_context.c +++ b/api/plgd/device-provisioning-client/plgd_dps_context.c @@ -26,6 +26,7 @@ #include "plgd_dps_resource_internal.h" #include "plgd_dps_store_internal.h" +#include "api/cloud/oc_cloud_schedule_internal.h" #include "oc_endpoint.h" #include "oc_session_events.h" #include "util/oc_list.h" diff --git a/api/plgd/device-provisioning-client/plgd_dps_resource.c b/api/plgd/device-provisioning-client/plgd_dps_resource.c index 68454a520..5813cd349 100644 --- a/api/plgd/device-provisioning-client/plgd_dps_resource.c +++ b/api/plgd/device-provisioning-client/plgd_dps_resource.c @@ -195,7 +195,7 @@ dps_resource_encode_iotivity_retry_timeouts(CborEncoder *encoder, oc_string_view_t retryKey = OC_STRING_VIEW(PLGD_DPS_TEST_IOTIVITY_RETRY); g_err |= oc_rep_encode_text_string(encoder, retryKey.data, retryKey.length); oc_rep_begin_array(encoder, retry); - for (size_t i = 0; i < DPS_ARRAY_SIZE(rtd->iotivity.retry_timeout); ++i) { + for (size_t i = 0; i < OC_ARRAY_SIZE(rtd->iotivity.retry_timeout); ++i) { if (rtd->iotivity.retry_timeout[i] == 0) { break; } @@ -278,7 +278,7 @@ dps_resource_encode_response(const plgd_dps_context_t *ctx, #ifdef PLGD_DPS_RESOURCE_TEST_PROPERTIES data.test.cloud_status_observer = ctx->cloud_observer.cfg; oc_cloud_get_retry_timeouts(&data.test.iotivity.retry_timeout[0], - DPS_ARRAY_SIZE(data.test.iotivity.retry_timeout)); + OC_ARRAY_SIZE(data.test.iotivity.retry_timeout)); #endif /* PLGD_DPS_RESOURCE_TEST_PROPERTIES */ dps_resource_encode(interface, ctx->conf, &data); @@ -311,7 +311,7 @@ dps_update_iotivity(const oc_rep_t *iot) if (oc_rep_get_int_array(iot, PLGD_DPS_TEST_IOTIVITY_RETRY, &retry, &retry_size)) { uint16_t retry_timeout[DPS_CLOUD_RETRY_TIMEOUTS_SIZE] = { 0 }; - for (size_t i = 0; i < retry_size && i < DPS_ARRAY_SIZE(retry_timeout); + for (size_t i = 0; i < retry_size && i < OC_ARRAY_SIZE(retry_timeout); ++i) { retry_timeout[i] = (uint16_t)retry[i]; } diff --git a/api/plgd/unittest/plgd_dps.cpp b/api/plgd/unittest/plgd_dps.cpp new file mode 100644 index 000000000..8fcd2c213 --- /dev/null +++ b/api/plgd/unittest/plgd_dps.cpp @@ -0,0 +1,201 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "plgd_dps_test.h" + +#include "api/plgd/device-provisioning-client/plgd_dps_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_security_internal.h" +#include "oc_api.h" +#include "oc_core_res.h" +#include "oc_uuid.h" + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static constexpr size_t kDeviceID = 0; + +TEST(DPSApiTest, SetSkipVerify) +{ + auto ctx = dps::make_unique_context(kDeviceID); + plgd_dps_set_skip_verify(ctx.get(), true); + + EXPECT_TRUE(plgd_dps_get_skip_verify(ctx.get())); +} + +TEST(DPSApiTest, StatusToLogString) +{ + EXPECT_NE(0, dps_status_to_logstr(0, nullptr, 0)); + std::array tooSmall{}; + EXPECT_NE(0, dps_status_to_logstr(0, &tooSmall[0], tooSmall.size())); + + std::vector buffer; + buffer.resize(1024); + EXPECT_EQ(0, dps_status_to_logstr(0, buffer.data(), buffer.capacity())); + EXPECT_STREQ(kPlgdDpsStatusUninitialized, buffer.data()); + + EXPECT_NE(0, dps_status_to_logstr(PLGD_DPS_PROVISIONED_ALL_FLAGS, + &tooSmall[0], tooSmall.size())); + + EXPECT_EQ(0, dps_status_to_logstr(PLGD_DPS_PROVISIONED_ALL_FLAGS, + buffer.data(), buffer.capacity())); + std::unordered_set flags{ + kPlgdDpsStatusInitialized, kPlgdDpsStatusGetTime, + kPlgdDpsStatusHasTime, kPlgdDpsStatusGetOwner, + kPlgdDpsStatusHasOwner, kPlgdDpsStatusGetCredentials, + kPlgdDpsStatusHasCredentials, kPlgdDpsStatusGetAcls, + kPlgdDpsStatusHasAcls, kPlgdDpsStatusGetCloud, + kPlgdDpsStatusHasCloud, kPlgdDpsStatusProvisioned, + kPlgdDpsStatusRenewCredentials, kPlgdDpsStatusTransientFailure, + kPlgdDpsStatusFailure, + }; + + std::stringstream ss{ buffer.data() }; + std::string s; + while (std::getline(ss, s, '|')) { + EXPECT_EQ(1, flags.erase(s)); + } + if (!flags.empty()) { + std::cout << "missing flags: "; + for (const auto &f : flags) { + std::cout << f << " "; + } + std::cout << std::endl; + } + EXPECT_TRUE(flags.empty()); +} + +class TestDPSWithDevice : public testing::Test { +private: + static int AppInit() + { + if (oc_init_platform("Samsung", nullptr, nullptr) != 0) { + return -1; + } + if (oc_add_device("/oic/d", "oic.d.light", "Lamp", "ocf.1.0.0", + "ocf.res.1.0.0", nullptr, nullptr) != 0) { + return -1; + } + return 0; + } + + static void SignalEventLoop() + { + // no-op for tests + } + +public: + void SetUp() override + { + static oc_handler_t handler{}; + handler.init = AppInit; + handler.signal_event_loop = SignalEventLoop; + ASSERT_EQ(0, oc_main_init(&handler)); + ASSERT_EQ(kDeviceID, oc_core_get_num_devices() - 1); + ASSERT_EQ(0, plgd_dps_init()); + } + void TearDown() override + { + plgd_dps_shutdown(); + oc_main_shutdown(); + } +}; + +TEST_F(TestDPSWithDevice, GetContext) +{ + EXPECT_NE(nullptr, plgd_dps_get_context(kDeviceID)); + + size_t invalidDeviceID = 42; + EXPECT_EQ(nullptr, plgd_dps_get_context(invalidDeviceID)); +} + +TEST_F(TestDPSWithDevice, SetSelfOwned) +{ + auto ctx = dps::make_unique_context(kDeviceID); + EXPECT_FALSE(dps_is_self_owned(ctx.get())); + + EXPECT_TRUE(dps_set_self_owned(ctx.get())); + EXPECT_TRUE(dps_is_self_owned(ctx.get())); + EXPECT_FALSE(dps_has_owner(ctx.get())); +} + +TEST_F(TestDPSWithDevice, SetOwned) +{ + auto ctx = dps::make_unique_context(kDeviceID); + EXPECT_FALSE(dps_has_owner(ctx.get())); + + oc_uuid_t owner; + oc_gen_uuid(&owner); + EXPECT_TRUE(dps_set_owner(ctx.get(), &owner)); + EXPECT_FALSE(dps_is_self_owned(ctx.get())); + EXPECT_TRUE(dps_has_owner(ctx.get())); +} + +TEST_F(TestDPSWithDevice, SetDpsResource) +{ + auto ctx = dps::make_unique_context(kDeviceID); + + auto hasDpsResource = [](size_t device) -> bool { + return oc_ri_get_app_resource_by_uri(PLGD_DPS_URI, sizeof(PLGD_DPS_URI) - 1, + device) != nullptr; + }; + EXPECT_FALSE(hasDpsResource(kDeviceID)); + + plgd_dps_set_configuration_resource(ctx.get(), false); + EXPECT_EQ(nullptr, ctx->conf); + EXPECT_FALSE(hasDpsResource(kDeviceID)); + + plgd_dps_set_configuration_resource(ctx.get(), true); + EXPECT_NE(nullptr, ctx->conf); + EXPECT_TRUE(hasDpsResource(kDeviceID)); + + plgd_dps_set_configuration_resource(ctx.get(), false); + EXPECT_EQ(nullptr, ctx->conf); + EXPECT_FALSE(hasDpsResource(kDeviceID)); +} + +TEST_F(TestDPSWithDevice, SetIdentityChain) +{ + // invalid deviceID + EXPECT_FALSE(dps_try_set_identity_chain(42)); +} + +TEST_F(TestDPSWithDevice, CloudAPI) +{ + EXPECT_FALSE(dps_cloud_is_started(kDeviceID)); + EXPECT_FALSE(dps_cloud_is_registered(kDeviceID)); + EXPECT_FALSE(dps_cloud_is_logged_in(kDeviceID)); + + size_t invalidDeviceID = 42; + EXPECT_FALSE(dps_cloud_is_started(invalidDeviceID)); + EXPECT_FALSE(dps_cloud_is_registered(invalidDeviceID)); + EXPECT_FALSE(dps_cloud_is_logged_in(invalidDeviceID)); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_context.cpp b/api/plgd/unittest/plgd_dps_context.cpp new file mode 100644 index 000000000..345ea43be --- /dev/null +++ b/api/plgd/unittest/plgd_dps_context.cpp @@ -0,0 +1,287 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/oc_runtime_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_context_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_internal.h" + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +static constexpr size_t kDeviceID = 0; + +class TestDPSWithContext : public testing::Test { +protected: + void SetUp() override + { + oc_runtime_init(); + memset(ctx_.get(), 0, sizeof(plgd_dps_context_t)); + dps_context_init(ctx_.get(), kDeviceID); + } + void TearDown() override + { + dps_context_deinit(ctx_.get()); + oc_runtime_shutdown(); + } + +public: + std::unique_ptr ctx_{ new plgd_dps_context_t }; +}; + +TEST_F(TestDPSWithContext, HasForcedReprovision) +{ + EXPECT_FALSE(plgd_dps_has_forced_reprovision(ctx_.get())); + + plgd_dps_force_reprovision(ctx_.get()); + EXPECT_TRUE(plgd_dps_has_forced_reprovision(ctx_.get())); +} + +TEST_F(TestDPSWithContext, HasBeenProvisionedSinceReset) +{ + EXPECT_FALSE(plgd_dps_has_been_provisioned_since_reset(ctx_.get())); + + dps_set_has_been_provisioned_since_reset(ctx_.get(), /*dump*/ false); + EXPECT_TRUE(plgd_dps_has_been_provisioned_since_reset(ctx_.get())); +} + +TEST_F(TestDPSWithContext, GetProvisionStatus) +{ + EXPECT_EQ(0, plgd_dps_get_provision_status(ctx_.get())); + + dps_set_ps_and_last_error(ctx_.get(), PLGD_DPS_INITIALIZED, 0, PLGD_DPS_OK); + EXPECT_EQ(PLGD_DPS_INITIALIZED, plgd_dps_get_provision_status(ctx_.get())); +} + +TEST_F(TestDPSWithContext, GetLastError) +{ + EXPECT_EQ(PLGD_DPS_OK, plgd_dps_get_last_error(ctx_.get())); + + dps_set_last_error(ctx_.get(), PLGD_DPS_ERROR_CONNECT); + EXPECT_EQ(PLGD_DPS_ERROR_CONNECT, plgd_dps_get_last_error(ctx_.get())); +} + +TEST_F(TestDPSWithContext, SetCloudObserver) +{ + plgd_cloud_status_observer_configuration_t cfg = + plgd_dps_get_cloud_observer_configuration(ctx_.get()); + EXPECT_EQ(30, cfg.max_count); + EXPECT_EQ(1, cfg.interval_s); + + EXPECT_FALSE(plgd_dps_set_cloud_observer_configuration( + ctx_.get(), /*max_retry_count*/ 0, /*retry_interval_s*/ 0)); + cfg = plgd_dps_get_cloud_observer_configuration(ctx_.get()); + EXPECT_EQ(30, cfg.max_count); + EXPECT_EQ(1, cfg.interval_s); + + EXPECT_TRUE(plgd_dps_set_cloud_observer_configuration( + ctx_.get(), /*max_retry_count*/ 13, /*retry_interval_s*/ 37)); + cfg = plgd_dps_get_cloud_observer_configuration(ctx_.get()); + EXPECT_EQ(13, cfg.max_count); + EXPECT_EQ(37, cfg.interval_s); +} + +TEST_F(TestDPSWithContext, SetExpiringLimit) +{ + const uint16_t expiresIn = 1337; + plgd_dps_pki_set_expiring_limit(ctx_.get(), expiresIn); + EXPECT_EQ(expiresIn, plgd_dps_pki_get_expiring_limit(ctx_.get())); +} + +TEST_F(TestDPSWithContext, SetEndpoint) +{ + std::array buffer{ '\0' }; + EXPECT_EQ(0, plgd_dps_get_endpoint(ctx_.get(), buffer.data(), buffer.size())); + + const char endpoint[] = "coaps+tcp://plgd.cloud:25684"; +#ifndef OC_DYNAMIC_ALLOCATION + ASSERT_GE(OC_ENDPOINT_MAX_ENDPOINT_URI_LENGTH, + std::string(endpoint).length()); +#endif /* OC_DYNAMIC_ALLOCATION */ + + plgd_dps_set_endpoint(ctx_.get(), endpoint); + std::array too_small{ '\0' }; + EXPECT_GT( + 0, plgd_dps_get_endpoint(ctx_.get(), too_small.data(), too_small.size())); + + size_t s = plgd_dps_get_endpoint(ctx_.get(), buffer.data(), buffer.size()); + EXPECT_EQ(sizeof(endpoint), s); + EXPECT_STREQ(endpoint, buffer.data()); +} + +TEST_F(TestDPSWithContext, SetRetry) +{ + EXPECT_FALSE(plgd_dps_set_retry_configuration(ctx_.get(), nullptr, 0)); + std::vector empty{}; + EXPECT_FALSE( + plgd_dps_set_retry_configuration(ctx_.get(), empty.data(), empty.size())); + std::array too_large; + EXPECT_FALSE(plgd_dps_set_retry_configuration(ctx_.get(), too_large.data(), + too_large.size())); + std::array empty_values = { 0 }; + EXPECT_FALSE(plgd_dps_set_retry_configuration(ctx_.get(), empty_values.data(), + empty_values.size())); + + std::array arr{ 1, 2, 3, 4, 5 }; + EXPECT_TRUE( + plgd_dps_set_retry_configuration(ctx_.get(), arr.data(), arr.size())); + std::array too_small; + size_t cfg_size = plgs_dps_get_retry_configuration( + ctx_.get(), too_small.data(), too_small.size()); + EXPECT_EQ(-1, cfg_size); + + std::array buffer{ 0 }; + auto EXPECT_ARR_EQ = [](const uint8_t *arr1, const uint8_t *arr2, + size_t size) { + for (size_t i = 0; i < size; ++i) { + EXPECT_EQ(arr1[i], arr2[i]) + << "Arrays differ at index " << i << " (" << (int)arr1[i] << " vs " + << (int)arr2[i] << ")"; + } + }; + std::array single{ 1 }; + EXPECT_TRUE( + plgd_dps_set_retry_configuration(ctx_.get(), single.data(), single.size())); + cfg_size = + plgs_dps_get_retry_configuration(ctx_.get(), buffer.data(), buffer.size()); + EXPECT_EQ(single.size(), cfg_size); + EXPECT_ARR_EQ(single.data(), buffer.data(), cfg_size); + + std::array full{ 1, 2, 3, 4, 5, 6, 7, 8 }; + EXPECT_TRUE( + plgd_dps_set_retry_configuration(ctx_.get(), full.data(), full.size())); + cfg_size = + plgs_dps_get_retry_configuration(ctx_.get(), buffer.data(), buffer.size()); + EXPECT_EQ(full.size(), cfg_size); + EXPECT_ARR_EQ(full.data(), buffer.data(), cfg_size); +} + +TEST_F(TestDPSWithContext, SetScheduleAction) +{ + bool cbk_called = false; + auto cbk = [](plgd_dps_context_t *, plgd_dps_status_t, uint8_t, uint64_t *, + uint16_t *, void *user_data) { + auto called = static_cast(user_data); + *called = true; + return false; + }; + plgd_dps_set_schedule_action(ctx_.get(), cbk, &cbk_called); + dps_retry_increment(ctx_.get(), PLGD_DPS_GET_CREDENTIALS); + EXPECT_EQ(0, ctx_.get()->retry.count); + EXPECT_TRUE(cbk_called); + plgd_dps_set_schedule_action(ctx_.get(), nullptr, nullptr); +} + +TEST_F(TestDPSWithContext, SetCertificateFingerprint) +{ + // no fingerprint set + std::array fingerprint_ok{ '\0' }; + mbedtls_md_type_t md_type = MBEDTLS_MD_NONE; + EXPECT_EQ(0, plgd_dps_get_certificate_fingerprint(ctx_.get(), &md_type, + &fingerprint_ok[0], + fingerprint_ok.size())); + + EXPECT_EQ(true, plgd_dps_set_certificate_fingerprint( + ctx_.get(), MBEDTLS_MD_NONE, nullptr, 0)); + + std::array fingerprint = { + (uint8_t)0xB8, (uint8_t)0xF5, (uint8_t)0xBA, (uint8_t)0x0D, (uint8_t)0x9F, + (uint8_t)0x6D, (uint8_t)0x4D, (uint8_t)0xEF, (uint8_t)0x3F, (uint8_t)0x82, + (uint8_t)0x28, (uint8_t)0xD2, (uint8_t)0x5F, (uint8_t)0x53, (uint8_t)0xD9, + (uint8_t)0x42, (uint8_t)0x8E, (uint8_t)0xAF, (uint8_t)0x0B, (uint8_t)0x36, + (uint8_t)0x71, (uint8_t)0xED, (uint8_t)0x80, (uint8_t)0xD7, (uint8_t)0x6C, + (uint8_t)0xE7, (uint8_t)0xDB, (uint8_t)0xAF, (uint8_t)0x44, (uint8_t)0xEC, + (uint8_t)0x28, (uint8_t)0xD3 + }; + + EXPECT_EQ(false, + plgd_dps_set_certificate_fingerprint( + ctx_.get(), MBEDTLS_MD_MD5, &fingerprint[0], fingerprint.size())); + EXPECT_EQ(false, plgd_dps_set_certificate_fingerprint( + ctx_.get(), MBEDTLS_MD_SHA256, &fingerprint[0], + fingerprint.size() + 1)); + EXPECT_EQ(true, plgd_dps_set_certificate_fingerprint( + ctx_.get(), MBEDTLS_MD_SHA256, &fingerprint[0], + fingerprint.size())); + + std::array fingerprint_too_small{ '\0' }; + EXPECT_EQ(-1, plgd_dps_get_certificate_fingerprint( + ctx_.get(), &md_type, &fingerprint_too_small[0], + fingerprint_too_small.size())); + + EXPECT_EQ(fingerprint_ok.size(), + plgd_dps_get_certificate_fingerprint( + ctx_.get(), &md_type, &fingerprint_ok[0], fingerprint_ok.size())); + EXPECT_EQ(MBEDTLS_MD_SHA256, md_type); + + EXPECT_EQ(fingerprint, fingerprint_ok); +} + +TEST_F(TestDPSWithContext, SetValuesFromVendorEncapsulatedOptions) +{ + std::string data = "c8:1c:63:6f:61:70:73:2b:74:63:70:3a:2f:2f:70:6c:67:64:2e:" + "63:6c:6f:75:64:3a:32:36:36:38:34:c9:20:" + "a1:e1:c3:4c:3e:3:17:8d:e4:77:79:f9:92:28:7d:fe:b4:b7:70:" + "2f:80:ee:d9:15:dd:ec:d6:" + "54:e4:c6:4f:e2:ca:6:53:48:41:32:35:36"; + ssize_t ret = + plgd_dps_hex_string_to_bytes(data.c_str(), data.length(), nullptr, 0); + ASSERT_EQ(72, ret); + std::array buf; + ret = plgd_dps_hex_string_to_bytes(data.c_str(), data.length(), &buf[0], ret); + ASSERT_EQ(72, ret); + EXPECT_EQ(PLGD_DPS_DHCP_SET_VALUES_ERROR, + plgd_dps_dhcp_set_values_from_vendor_encapsulated_options( + ctx_.get(), &buf[0], ret - 1)); + // last byte is '6' from SHA256 + buf[ret - 1] = 'A'; + // invalid SHA25A + EXPECT_EQ(PLGD_DPS_DHCP_SET_VALUES_ERROR, + plgd_dps_dhcp_set_values_from_vendor_encapsulated_options( + ctx_.get(), &buf[0], ret)); + buf[ret - 1] = '6'; + EXPECT_EQ(PLGD_DPS_DHCP_SET_VALUES_NEED_REPROVISION, + plgd_dps_dhcp_set_values_from_vendor_encapsulated_options( + ctx_.get(), &buf[0], ret)); + EXPECT_EQ(PLGD_DPS_DHCP_SET_VALUES_NOT_CHANGED, + plgd_dps_dhcp_set_values_from_vendor_encapsulated_options( + ctx_.get(), &buf[0], ret)); + std::string data1 = "c8:1c:63:6f:61:70:73:2b:74:63:70:3a:2f:2f:70:6c:67:64:" + "2e:63:6c:6f:75:64:3a:32:36:36:38:34:c9:20:" + "a1:e1:c3:4c:3e:3:17:8d:e4:77:79:f9:92:28:7d:fe:b4:b7:70:" + "2f:81:ee:d9:15:dd:ec:d6:" + "54:e4:c6:4f:e2:ca:6:53:48:41:32:35:36"; + std::array buf1; + ret = plgd_dps_hex_string_to_bytes(data1.c_str(), data1.length(), &buf1[0], + buf1.size()); + ASSERT_EQ(72, ret); + ctx_.get()->status = PLGD_DPS_PROVISIONED_MASK; + EXPECT_EQ(PLGD_DPS_DHCP_SET_VALUES_UPDATED, + plgd_dps_dhcp_set_values_from_vendor_encapsulated_options( + ctx_.get(), &buf1[0], ret)); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ \ No newline at end of file diff --git a/api/plgd/unittest/plgd_dps_provision.cpp b/api/plgd/unittest/plgd_dps_provision.cpp new file mode 100644 index 000000000..cc7ea5b60 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_provision.cpp @@ -0,0 +1,176 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/plgd/device-provisioning-client/plgd_dps_provision_cloud_internal.h" +#include "oc_rep.h" +#include "tests/gtest/RepPool.h" + +#include "gtest/gtest.h" + +#include +#include +#include + +struct cloudEndpoint +{ + std::string uri; + std::string id; +}; + +static struct cloudEndpoint +makeCloudEndpoint(std::string uri, std::string id) +{ + return cloudEndpoint{ uri, id }; +} + +static void +makeCloudConfigurationPayload(const std::string &at, const std::string &apn, + const std::string &cis, const std::string &sid, + const std::vector &endpoints = {}) +{ + oc_rep_start_root_object(); + EXPECT_EQ(CborNoError, oc_rep_get_cbor_errno()); + if (!at.empty()) { + oc_rep_set_text_string(root, at, at.c_str()); + EXPECT_EQ(CborNoError, oc_rep_get_cbor_errno()); + } + if (!apn.empty()) { + oc_rep_set_text_string(root, apn, apn.c_str()); + EXPECT_EQ(CborNoError, oc_rep_get_cbor_errno()); + } + if (!cis.empty()) { + oc_rep_set_text_string(root, cis, cis.c_str()); + EXPECT_EQ(CborNoError, oc_rep_get_cbor_errno()); + } + if (!sid.empty()) { + oc_rep_set_text_string(root, sid, sid.c_str()); + EXPECT_EQ(CborNoError, oc_rep_get_cbor_errno()); + } + if (!endpoints.empty()) { + std::string epsKey = "x.org.iotivity.servers"; + g_err |= oc_rep_encode_text_string(oc_rep_object(root), epsKey.c_str(), + epsKey.length()); + oc_rep_begin_array(oc_rep_object(root), endpoints); + for (const auto &endpoint : endpoints) { + oc_rep_object_array_start_item(endpoints); + oc_rep_set_text_string(endpoints, uri, endpoint.uri.c_str()); + oc_rep_set_text_string(endpoints, id, endpoint.id.c_str()); + oc_rep_object_array_end_item(endpoints); + } + oc_rep_end_array(oc_rep_object(root), endpoints); + } + + oc_rep_end_root_object(); + EXPECT_EQ(CborNoError, oc_rep_get_cbor_errno()); +} + +TEST(DPSFillCloudTest, MissingAccessToken) +{ + oc::RepPool pool{}; + + makeCloudConfigurationPayload("", "auth_provider", "server", "server_id"); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + cloud_conf_t cloud{}; + EXPECT_FALSE(dps_register_cloud_fill_data(rep.get(), &cloud)); +} + +TEST(DPSFillCloudTest, MissingAuthProvider) +{ + oc::RepPool pool{}; + + makeCloudConfigurationPayload("access_token", "", "server", "server_id"); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + cloud_conf_t cloud{}; + EXPECT_FALSE(dps_register_cloud_fill_data(rep.get(), &cloud)); +} + +TEST(DPSFillCloudTest, MissingServer) +{ + oc::RepPool pool{}; + + makeCloudConfigurationPayload("access_token", "auth_provider", "", + "server_id"); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + cloud_conf_t cloud{}; + EXPECT_FALSE(dps_register_cloud_fill_data(rep.get(), &cloud)); +} + +TEST(DPSFillCloudTest, MissingServerID) +{ + oc::RepPool pool{}; + + makeCloudConfigurationPayload("access_token", "auth_provider", "server", ""); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + cloud_conf_t cloud{}; + EXPECT_FALSE(dps_register_cloud_fill_data(rep.get(), &cloud)); +} + +TEST(DPSFillCloudTest, FillSuccess) +{ + oc::RepPool pool{}; + + std::vector endpoints = { makeCloudEndpoint("uri/1", "id1"), + makeCloudEndpoint("uri/2", "id2") }; + makeCloudConfigurationPayload("access_token", "auth_provider", "server", + "server id", endpoints); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + cloud_conf_t cloud{}; + EXPECT_TRUE(dps_register_cloud_fill_data(rep.get(), &cloud)); + + EXPECT_STREQ("access_token", oc_string(*cloud.access_token)); + EXPECT_STREQ("auth_provider", oc_string(*cloud.auth_provider)); + EXPECT_STREQ("server", oc_string(*cloud.ci_server)); + EXPECT_STREQ("server id", oc_string(*cloud.sid)); + EXPECT_NE(nullptr, cloud.ci_servers); + size_t count = 0; + for (const oc_rep_t *server = cloud.ci_servers; server != nullptr; + server = server->next) { + std::string_view uriKey = "uri"; + const oc_rep_t *prop = oc_rep_get_by_type_and_key( + server->value.object, OC_REP_STRING, uriKey.data(), uriKey.length()); + ASSERT_NE(nullptr, prop); + ASSERT_NE(nullptr, oc_string(prop->value.string)); + std::string uri = oc_string(prop->value.string); + + std::string_view idKey = "id"; + prop = oc_rep_get_by_type_and_key(server->value.object, OC_REP_STRING, + idKey.data(), idKey.length()); + ASSERT_NE(nullptr, prop); + ASSERT_NE(nullptr, oc_string(prop->value.string)); + std::string id = oc_string(prop->value.string); + + EXPECT_NE(endpoints.end(), std::find_if(endpoints.begin(), endpoints.end(), + [&](const cloudEndpoint &endpoint) { + return endpoint.uri == uri && + endpoint.id == id; + })); + ++count; + } + EXPECT_EQ(endpoints.size(), count); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_provision_cloud.cpp b/api/plgd/unittest/plgd_dps_provision_cloud.cpp new file mode 100644 index 000000000..36580ce8a --- /dev/null +++ b/api/plgd/unittest/plgd_dps_provision_cloud.cpp @@ -0,0 +1,380 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/cloud/oc_cloud_context_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_context_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_log_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_manager_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_provision_cloud_internal.h" +#include "oc_api.h" +#include "oc_cloud.h" +#include "oc_core_res.h" +#include "oc_helpers.h" +#include "oc_rep.h" +#include "oc_uuid.h" +#include "security/oc_pstat_internal.h" +#include "tests/gtest/Device.h" +#include "tests/gtest/Endpoint.h" +#include "tests/gtest/RepPool.h" + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +static constexpr size_t kDeviceID = 0; + +class DPSProvisionCloudWithServerTest : public testing::Test { +public: + static void SetUpTestCase() + { + EXPECT_TRUE(oc::TestDevice::StartServer()); + plgd_dps_init(); + } + + static void TearDownTestCase() + { + plgd_dps_shutdown(); + oc_sec_pstat_t *pstat = oc_sec_get_pstat(kDeviceID); + pstat->s = OC_DOS_RFOTM; + oc::TestDevice::StopServer(); + } + + void SetUp() override + { + oc_sec_pstat_t *pstat = oc_sec_get_pstat(kDeviceID); + pstat->s = OC_DOS_RFNOP; + } + + void TearDown() override + { + oc::TestDevice::Reset(); + auto cloud_ctx = oc_cloud_get_context(kDeviceID); + ASSERT_NE(nullptr, cloud_ctx); + oc_cloud_context_clear(cloud_ctx, false); + } + + static void clearCloudServers(size_t device) + { + auto cloud_ctx = oc_cloud_get_context(device); + ASSERT_NE(nullptr, cloud_ctx); + do { + const oc_endpoint_address_t *ea = + oc_cloud_selected_server_address(cloud_ctx); + if (ea == nullptr) { + break; + } + oc_cloud_remove_server_address(cloud_ctx, ea); + } while (true); + } + + static int encodeConfResourcePayload(const std::string &cis, + const std::string &sid, + const std::string &at, + const std::string &apn) + { + oc_rep_begin_root_object(); + oc_rep_set_text_string(root, cis, cis.c_str()); + oc_rep_set_text_string(root, sid, sid.c_str()); + oc_rep_set_text_string(root, at, at.c_str()); + oc_rep_set_text_string(root, apn, apn.c_str()); + oc_rep_end_root_object(); + return oc_rep_get_cbor_errno(); + } +}; + +TEST_F(DPSProvisionCloudWithServerTest, HandleSetCloudResponseFail) +{ + auto ctx = std::make_unique(); + oc_client_response_t data{}; + data.user_data = ctx.get(); + data.code = OC_STATUS_OK; + // invalid payload + EXPECT_EQ(PLGD_DPS_ERROR_RESPONSE, dps_handle_set_cloud_response(&data)); + + oc::RepPool pool{}; + + // invalid sid + ctx->device = kDeviceID; + ASSERT_EQ(CborNoError, + encodeConfResourcePayload("cis", "not-an-UUID", "at", "apn")); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + data.payload = rep.get(); + EXPECT_EQ(PLGD_DPS_ERROR_SET_CLOUD, dps_handle_set_cloud_response(&data)); + rep.reset(); + pool.Clear(); + + ASSERT_EQ(CborNoError, + encodeConfResourcePayload( + "cis", "00000000-0000-0000-0000-000000000001", "at", "apn")); + rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + data.payload = rep.get(); + // invalid device + ctx->device = 42; + EXPECT_EQ(PLGD_DPS_ERROR_SET_CLOUD, dps_handle_set_cloud_response(&data)); + + ctx->device = kDeviceID; + // logged in and no cloud server set + auto cloud_ctx = oc_cloud_get_context(kDeviceID); + ASSERT_NE(nullptr, cloud_ctx); + cloud_ctx->store.status = OC_CLOUD_REGISTERED | OC_CLOUD_LOGGED_IN; + clearCloudServers(kDeviceID); + EXPECT_EQ(PLGD_DPS_ERROR_SET_CLOUD, dps_handle_set_cloud_response(&data)); +} + +TEST_F(DPSProvisionCloudWithServerTest, HandleSetCloudResponse) +{ + oc::RepPool pool{}; + + std::string cis = "cis"; + std::string sid = "00000000-0000-0000-0000-000000000001"; + std::string at = "at"; + std::string apn = "apn"; + ASSERT_EQ(CborNoError, encodeConfResourcePayload(cis, sid, at, apn)); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + + auto ctx = std::make_unique(); + oc_client_response_t data{}; + data.user_data = ctx.get(); + data.code = OC_STATUS_OK; + data.payload = rep.get(); + EXPECT_EQ(PLGD_DPS_OK, dps_handle_set_cloud_response(&data)); + + // update signed in cloud with same data + oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(ctx->device); + ASSERT_NE(nullptr, cloud_ctx); + cloud_ctx->store.status = OC_CLOUD_REGISTERED | OC_CLOUD_LOGGED_IN; + EXPECT_EQ(PLGD_DPS_OK, dps_handle_set_cloud_response(&data)); + rep.reset(); + pool.Clear(); + + // update signed in cloud with different cis + cloud_ctx->store.status = OC_CLOUD_REGISTERED | OC_CLOUD_LOGGED_IN; + cis = "nextcis"; + ASSERT_EQ(CborNoError, encodeConfResourcePayload(cis, sid, at, apn)); + rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + data.payload = rep.get(); + EXPECT_EQ(PLGD_DPS_OK, dps_handle_set_cloud_response(&data)); + rep.reset(); + pool.Clear(); + + // update signed in cloud with different apn + cloud_ctx->store.status = OC_CLOUD_REGISTERED | OC_CLOUD_LOGGED_IN; + apn = "nextapn"; + ASSERT_EQ(CborNoError, encodeConfResourcePayload(cis, sid, at, apn)); + rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + data.payload = rep.get(); + EXPECT_EQ(PLGD_DPS_OK, dps_handle_set_cloud_response(&data)); + rep.reset(); + pool.Clear(); + + // update signed in cloud with different sid, but not connected to a cloud + cloud_ctx->store.status = OC_CLOUD_REGISTERED | OC_CLOUD_LOGGED_IN; + sid = "00000000-0000-0000-0000-000000000002"; + ASSERT_EQ(CborNoError, encodeConfResourcePayload(cis, sid, at, apn)); + rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + data.payload = rep.get(); + EXPECT_EQ(PLGD_DPS_OK, dps_handle_set_cloud_response(&data)); + rep.reset(); + pool.Clear(); + + // update signed in cloud with different sid and connected to a cloud + cloud_ctx->store.status = OC_CLOUD_REGISTERED | OC_CLOUD_LOGGED_IN; + std::string uid = "501"; + oc_set_string(&cloud_ctx->store.uid, uid.c_str(), uid.length()); + sid = "00000000-0000-0000-0000-000000000003"; + oc_endpoint_t ep = oc::endpoint::FromString("coap://[ff02::158]"); + memcpy(cloud_ctx->cloud_ep, &ep, sizeof(oc_endpoint_t)); + cloud_ctx->cloud_ep_state = OC_SESSION_CONNECTED; + ASSERT_EQ(CborNoError, encodeConfResourcePayload(cis, sid, at, apn)); + rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + data.payload = rep.get(); + EXPECT_EQ(PLGD_DPS_OK, dps_handle_set_cloud_response(&data)); + EXPECT_EQ(OC_SESSION_DISCONNECTED, cloud_ctx->cloud_ep_state); +} + +TEST_F(DPSProvisionCloudWithServerTest, HasCloudConfiguration) +{ + // invalid device + EXPECT_FALSE(dps_has_cloud_configuration(42)); + + auto cloud_ctx = oc_cloud_get_context(kDeviceID); + ASSERT_NE(nullptr, cloud_ctx); + + // missing access token + oc_cloud_context_clear(cloud_ctx, false); + ASSERT_EQ(nullptr, oc_string(*oc_cloud_get_access_token(cloud_ctx))); + EXPECT_FALSE(dps_has_cloud_configuration(kDeviceID)); + + // no selected gateway + clearCloudServers(kDeviceID); + EXPECT_FALSE(dps_has_cloud_configuration(kDeviceID)); + + // ok + ASSERT_EQ(0, oc_cloud_provision_conf_resource(cloud_ctx, "coap://[ff02::158]", + "access_token", "", "")); + EXPECT_TRUE(dps_has_cloud_configuration(kDeviceID)); +} + +TEST_F(DPSProvisionCloudWithServerTest, SetCloudHandler) +{ + auto ctx = std::make_unique(); + oc_client_response_t data{}; + data.user_data = ctx.get(); + data.code = OC_STATUS_OK; + + // if PLGD_DPS_HAS_CLOUD is not set then PLGD_DPS_GET_CLOUD must be set + ctx->status = PLGD_DPS_INITIALIZED | PLGD_DPS_HAS_TIME | PLGD_DPS_HAS_OWNER; + dps_set_cloud_handler(&data); + EXPECT_EQ(PLGD_DPS_ERROR_SET_CLOUD, ctx->last_error); + EXPECT_NE(0, ctx->status & PLGD_DPS_FAILURE); + + // returned error code + ctx->status = PLGD_DPS_INITIALIZED | PLGD_DPS_HAS_TIME | PLGD_DPS_HAS_OWNER | + PLGD_DPS_GET_CLOUD; + data.code = OC_STATUS_BAD_REQUEST; + dps_set_cloud_handler(&data); + EXPECT_EQ(PLGD_DPS_ERROR_RESPONSE, ctx->last_error); + EXPECT_NE(0, ctx->status & PLGD_DPS_FAILURE); + + // invalid payload + data.code = OC_STATUS_OK; + data.payload = nullptr; + dps_set_cloud_handler(&data); + EXPECT_EQ(PLGD_DPS_ERROR_RESPONSE, ctx->last_error); + EXPECT_NE(0, ctx->status & PLGD_DPS_FAILURE); + + // ok + oc::RepPool pool{}; + ASSERT_EQ(CborNoError, + encodeConfResourcePayload( + "cis", "00000000-0000-0000-0000-000000000001", "at", "apn")); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + data.payload = rep.get(); + dps_set_cloud_handler(&data); + EXPECT_EQ(PLGD_DPS_OK, ctx->last_error); + EXPECT_EQ(PLGD_DPS_INITIALIZED | PLGD_DPS_HAS_TIME | PLGD_DPS_HAS_OWNER | + PLGD_DPS_HAS_CLOUD, + ctx->status); + + // if PLGD_DPS_HAS_CLOUD flag is already set then nothing should be done + ctx->status = PLGD_DPS_INITIALIZED | PLGD_DPS_HAS_TIME | PLGD_DPS_HAS_OWNER | + PLGD_DPS_HAS_CLOUD; + data.payload = nullptr; + dps_set_cloud_handler(&data); + EXPECT_EQ(PLGD_DPS_OK, ctx->last_error); + + dps_manager_stop(ctx.get()); +} + +TEST_F(DPSProvisionCloudWithServerTest, SetCloudEncodeSelectedGatewayFail) +{ + auto ctx = std::make_unique(); + ctx->device = 42; + EXPECT_FALSE(dps_provisioning_set_cloud_encode_selected_gateway(ctx.get())); + + oc::RepPool pool{ 1 }; + ctx->device = kDeviceID; + oc_rep_begin_root_object(); + EXPECT_FALSE(dps_provisioning_set_cloud_encode_selected_gateway(ctx.get())); +} + +TEST_F(DPSProvisionCloudWithServerTest, SetCloudEncodeSelectedGateway) +{ + oc::RepPool pool{}; + auto ctx = std::make_unique(); + ctx->device = kDeviceID; + + // with selected gateway + oc_rep_begin_root_object(); + EXPECT_TRUE(dps_provisioning_set_cloud_encode_selected_gateway(ctx.get())); + oc_rep_end_root_object(); + auto rep = pool.ParsePayload(); + DPS_DBG("%s", oc::RepPool::GetJson(rep.get(), true).data()); + + rep.reset(); + pool.Clear(); + + // without selected gateway and selected gateway id + clearCloudServers(kDeviceID); + + oc_rep_begin_root_object(); + EXPECT_TRUE(dps_provisioning_set_cloud_encode_selected_gateway(ctx.get())); + oc_rep_end_root_object(); + rep = pool.ParsePayload(); + DPS_DBG("%s", oc::RepPool::GetJson(rep.get(), true).data()); +} + +TEST_F(DPSProvisionCloudWithServerTest, SetCloudEncodePayloadFail) +{ + auto ctx = std::make_unique(); + ctx->device = 42; + EXPECT_FALSE(dps_provisioning_set_cloud_encode_payload(ctx.get())); + + oc::RepPool pool{ 1 }; + ctx->device = kDeviceID; + EXPECT_FALSE(dps_provisioning_set_cloud_encode_payload(ctx.get())); +} + +TEST_F(DPSProvisionCloudWithServerTest, SetCloudEncodePayload) +{ + oc::RepPool pool{}; + auto ctx = std::make_unique(); + ctx->device = kDeviceID; + EXPECT_TRUE(dps_provisioning_set_cloud_encode_payload(ctx.get())); +} + +TEST_F(DPSProvisionCloudWithServerTest, SetCloudFail) +{ + auto ctx = std::make_unique(); + ctx->device = kDeviceID; + oc_endpoint_t ep = oc::endpoint::FromString("coap://[ff02::158]"); + ctx->endpoint = &ep; + + // must be in RFNOP state + oc_sec_pstat_t *pstat = oc_sec_get_pstat(kDeviceID); + pstat->s = OC_DOS_RFOTM; + EXPECT_FALSE(dps_provisioning_set_cloud(ctx.get())); +} + +TEST_F(DPSProvisionCloudWithServerTest, SetCloud) +{ + auto ctx = std::make_unique(); + ctx->device = kDeviceID; + oc_endpoint_t ep = oc::endpoint::FromString("coap://[ff02::158]"); + ctx->endpoint = &ep; + + EXPECT_TRUE(dps_provisioning_set_cloud(ctx.get())); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ \ No newline at end of file diff --git a/api/plgd/unittest/plgd_dps_resource.cpp b/api/plgd/unittest/plgd_dps_resource.cpp new file mode 100644 index 000000000..fd67f1820 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_resource.cpp @@ -0,0 +1,562 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "util/oc_features.h" + +#if defined(OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING) && \ + defined(OC_HAS_FEATURE_RESOURCE_ACCESS_IN_RFOTM) + +#include "api/oc_rep_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_context_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_endpoints_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_log_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_resource_internal.h" +#include "tests/gtest/Device.h" +#include "tests/gtest/RepPool.h" +#include "tests/gtest/Resource.h" +#include "util/oc_macros_internal.h" + +#include "gtest/gtest.h" + +#include +#include +#include + +static constexpr size_t kDeviceID = 0; + +using namespace std::chrono_literals; + +class DPSResourceTest : public testing::Test {}; + +struct DPSEndpointData +{ + std::string uri; + std::string name; +}; + +struct DPSData +{ + std::optional lastErrorCode; + std::optional provisionStatus; + DPSEndpointData endpoint; + std::vector endpoints; + bool forceReprovision; +}; + +static std::vector +parseDPSEndpoints(const oc_rep_t *servers) +{ + std::vector endpoints{}; + for (const oc_rep_t *server = servers; server != nullptr; + server = server->next) { + DPSEndpointData data{}; + const oc_rep_t *rep = oc_rep_get_by_type_and_key( + server->value.object, OC_REP_STRING, "uri", OC_CHAR_ARRAY_LEN("uri")); + if (rep == nullptr) { + return {}; + } + data.uri = oc_string(rep->value.string); + + rep = oc_rep_get_by_type_and_key(server->value.object, OC_REP_STRING, + "name", OC_CHAR_ARRAY_LEN("name")); + if (rep == nullptr) { + return {}; + } + data.name = oc_string(rep->value.string); + endpoints.push_back(data); + } + return endpoints; +} + +static bool +parseDPSDataProperty(const oc_rep_t *rep, DPSData &dpsData) +{ + if (rep->type == OC_REP_BOOL) { + if (std::string(oc_string(rep->name)) == "forceReprovision") { + dpsData.forceReprovision = rep->value.boolean; + return true; + } + return false; + } + + if (rep->type == OC_REP_INT) { + if (std::string(oc_string(rep->name)) == "lastErrorCode") { + dpsData.lastErrorCode = static_cast(rep->value.integer); + return true; + } + return false; + } + + if (rep->type == OC_REP_STRING) { + if (std::string(oc_string(rep->name)) == "endpoint") { + dpsData.endpoint.uri = oc_string(rep->value.string); + return true; + } + if (std::string(oc_string(rep->name)) == "endpointName") { + dpsData.endpoint.name = oc_string(rep->value.string); + return true; + } + if (std::string(oc_string(rep->name)) == "provisionStatus") { + dpsData.provisionStatus = oc_string(rep->value.string); + return true; + } + return false; + } + + if (rep->type == OC_REP_OBJECT_ARRAY) { + if (std::string(oc_string(rep->name)) == "endpoints") { + dpsData.endpoints = parseDPSEndpoints(rep->value.object_array); + return true; + } + return false; + } + +#ifdef PLGD_DPS_RESOURCE_TEST_PROPERTIES + if (rep->type == OC_REP_OBJECT) { + if (std::string(oc_string(rep->name)) == "test") { + return true; + } + return false; + } +#endif /* PLGD_DPS_RESOURCE_TEST_PROPERTIES */ + + return false; +} + +static bool +parseDPSData(const oc_rep_t *rep, DPSData &dpsData) +{ + for (; rep != nullptr; rep = rep->next) { + if (oc_rep_is_baseline_interface_property(rep)) { + continue; + } + + if (parseDPSDataProperty(rep, dpsData)) { + continue; + } + DPS_DBG("Unexpected property: %s\n", oc_string(rep->name)); + } + return true; +} + +TEST_F(DPSResourceTest, EncodeRead) +{ + oc::RepPool pool{}; + + dps_resource_data_t data{}; + data.last_error = PLGD_DPS_ERROR_RESPONSE; + std::string status = kPlgdDpsStatusProvisioned; + data.provision_status = status.c_str(); + data.provision_status_length = status.length(); + oc_endpoint_addresses_t ea{}; + ASSERT_TRUE(dps_endpoints_init(&ea, nullptr, nullptr)); + data.endpoints = &ea; + std::string ep1_uri = "coap://[::1]:42"; + std::string ep1_name = "name"; + auto *ep1 = oc_endpoint_addresses_add( + &ea, oc_endpoint_address_make_view_with_name( + oc_string_view(ep1_uri.c_str(), ep1_uri.length()), + oc_string_view(ep1_name.c_str(), ep1_name.length()))); + ASSERT_NE(nullptr, ep1); + data.forceReprovision = true; + dps_resource_encode(OC_IF_R, nullptr, &data); + + auto rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + DPSData parsed{}; + ASSERT_TRUE(parseDPSData(rep.get(), parsed)); + ASSERT_TRUE(parsed.lastErrorCode.has_value()); + EXPECT_EQ(data.last_error, *parsed.lastErrorCode); + ASSERT_TRUE(parsed.provisionStatus.has_value()); + EXPECT_STREQ(data.provision_status, parsed.provisionStatus->c_str()); + EXPECT_STREQ(ep1_uri.c_str(), parsed.endpoint.uri.c_str()); + EXPECT_STREQ(ep1_name.c_str(), parsed.endpoint.name.c_str()); + EXPECT_EQ(0, parsed.endpoints.size()); + EXPECT_EQ(data.forceReprovision, parsed.forceReprovision); + + // no status + rep.reset(); + pool.Clear(); + data.provision_status = nullptr; + data.provision_status_length = 0; + dps_resource_encode(OC_IF_R, nullptr, &data); + + rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + parsed = {}; + ASSERT_TRUE(parseDPSData(rep.get(), parsed)); + ASSERT_TRUE(parsed.lastErrorCode.has_value()); + EXPECT_EQ(data.last_error, *parsed.lastErrorCode); + ASSERT_FALSE(parsed.provisionStatus.has_value()); + EXPECT_STREQ(ep1_uri.c_str(), parsed.endpoint.uri.c_str()); + EXPECT_STREQ(ep1_name.c_str(), parsed.endpoint.name.c_str()); + EXPECT_EQ(0, parsed.endpoints.size()); + EXPECT_EQ(data.forceReprovision, parsed.forceReprovision); + + // multiple endpoints + rep.reset(); + pool.Clear(); + std::string ep2_uri = "coap://[::1]:43"; + std::string ep2_name = "name2"; + auto *ep2 = oc_endpoint_addresses_add( + &ea, oc_endpoint_address_make_view_with_name( + oc_string_view(ep2_uri.c_str(), ep2_uri.length()), + oc_string_view(ep2_name.c_str(), ep2_name.length()))); + ASSERT_NE(nullptr, ep2); + oc_endpoint_addresses_select(&ea, ep2); + dps_resource_encode(OC_IF_R, nullptr, &data); + + rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + parsed = {}; + ASSERT_TRUE(parseDPSData(rep.get(), parsed)); + ASSERT_TRUE(parsed.lastErrorCode.has_value()); + EXPECT_EQ(data.last_error, *parsed.lastErrorCode); + ASSERT_FALSE(parsed.provisionStatus.has_value()); + EXPECT_STREQ(ep2_uri.c_str(), parsed.endpoint.uri.c_str()); + EXPECT_STREQ(ep2_name.c_str(), parsed.endpoint.name.c_str()); + EXPECT_EQ(2, parsed.endpoints.size()); + EXPECT_EQ(data.forceReprovision, parsed.forceReprovision); + + oc_endpoint_addresses_deinit(&ea); +} + +TEST_F(DPSResourceTest, EncodeReadWrite) +{ + oc::RepPool pool{}; + + dps_resource_data_t data{}; + data.last_error = PLGD_DPS_ERROR_RESPONSE; + std::string status = kPlgdDpsStatusProvisioned; + data.provision_status = status.c_str(); + data.provision_status_length = status.length(); + oc_endpoint_addresses_t ea{}; + ASSERT_TRUE(dps_endpoints_init(&ea, nullptr, nullptr)); + data.endpoints = &ea; + std::string endpoint_uri = "coap://[::1]:42"; + std::string endpoint_name = "name"; + ASSERT_NE( + nullptr, + oc_endpoint_addresses_add( + &ea, oc_endpoint_address_make_view_with_name( + oc_string_view(endpoint_uri.c_str(), endpoint_uri.length()), + oc_string_view(endpoint_name.c_str(), endpoint_name.length())))); + data.forceReprovision = true; + dps_resource_encode(OC_IF_RW, nullptr, &data); + + auto rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + + DPSData parsed{}; + ASSERT_TRUE(parseDPSData(rep.get(), parsed)); + ASSERT_FALSE(parsed.lastErrorCode.has_value()); + ASSERT_FALSE(parsed.provisionStatus.has_value()); + EXPECT_STREQ(endpoint_uri.c_str(), parsed.endpoint.uri.c_str()); + EXPECT_STREQ(endpoint_name.c_str(), parsed.endpoint.name.c_str()); + EXPECT_EQ(data.forceReprovision, parsed.forceReprovision); + + oc_endpoint_addresses_deinit(&ea); +} + +TEST_F(DPSResourceTest, StatusToString) +{ + EXPECT_STREQ(dps_status_to_str(0), kPlgdDpsStatusUninitialized); + EXPECT_STREQ(dps_status_to_str(PLGD_DPS_INITIALIZED), + kPlgdDpsStatusInitialized); + EXPECT_STREQ(dps_status_to_str(PLGD_DPS_INITIALIZED | PLGD_DPS_FAILURE), + kPlgdDpsStatusFailure); + + uint32_t status = PLGD_DPS_INITIALIZED; + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_GET_TIME), + kPlgdDpsStatusGetTime); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_GET_TIME | PLGD_DPS_TRANSIENT_FAILURE), + kPlgdDpsStatusTransientFailure); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_HAS_TIME), + kPlgdDpsStatusHasTime); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_HAS_TIME | PLGD_DPS_FAILURE), + kPlgdDpsStatusFailure); + status |= PLGD_DPS_HAS_TIME; + + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_GET_OWNER), + kPlgdDpsStatusGetOwner); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_GET_OWNER | PLGD_DPS_TRANSIENT_FAILURE), + kPlgdDpsStatusTransientFailure); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_HAS_OWNER), + kPlgdDpsStatusHasOwner); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_HAS_OWNER | PLGD_DPS_FAILURE), + kPlgdDpsStatusFailure); + status |= PLGD_DPS_HAS_OWNER; + + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_GET_CLOUD), + kPlgdDpsStatusGetCloud); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_GET_CLOUD | PLGD_DPS_TRANSIENT_FAILURE), + kPlgdDpsStatusTransientFailure); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_HAS_CLOUD), + kPlgdDpsStatusHasCloud); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_HAS_CLOUD | PLGD_DPS_FAILURE), + kPlgdDpsStatusFailure); + status |= PLGD_DPS_HAS_CLOUD; + + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_GET_CREDENTIALS), + kPlgdDpsStatusGetCredentials); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_GET_CREDENTIALS | + PLGD_DPS_TRANSIENT_FAILURE), + kPlgdDpsStatusTransientFailure); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_HAS_CREDENTIALS), + kPlgdDpsStatusHasCredentials); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_HAS_CREDENTIALS | PLGD_DPS_FAILURE), + kPlgdDpsStatusFailure); + status |= PLGD_DPS_HAS_CREDENTIALS; + + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_GET_ACLS), + kPlgdDpsStatusGetAcls); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_GET_ACLS | PLGD_DPS_TRANSIENT_FAILURE), + kPlgdDpsStatusTransientFailure); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_HAS_ACLS), + kPlgdDpsStatusHasAcls); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_HAS_ACLS | PLGD_DPS_FAILURE), + kPlgdDpsStatusFailure); + status |= PLGD_DPS_HAS_ACLS; + + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_CLOUD_STARTED), + kPlgdDpsStatusProvisioned); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_CLOUD_STARTED | + PLGD_DPS_TRANSIENT_FAILURE), + kPlgdDpsStatusTransientFailure); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_CLOUD_STARTED | PLGD_DPS_FAILURE), + kPlgdDpsStatusFailure); + status |= PLGD_DPS_CLOUD_STARTED; + + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_RENEW_CREDENTIALS), + kPlgdDpsStatusRenewCredentials); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_RENEW_CREDENTIALS | + PLGD_DPS_TRANSIENT_FAILURE), + kPlgdDpsStatusTransientFailure); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_RENEW_CREDENTIALS | PLGD_DPS_FAILURE), + kPlgdDpsStatusFailure); + + for (uint8_t i = 0; i < 32; ++i) { + uint32_t flag = (1 << i); + if ((PLGD_DPS_PROVISIONED_ALL_FLAGS & flag) == 0) { + EXPECT_EQ(nullptr, dps_status_to_str(flag)); + } + } +} + +class DPSResourceTestWithServer : public testing::Test { +public: + static void SetUpDPS() + { + plgd_dps_init(); + plgd_dps_context_t *dps_ctx = plgd_dps_get_context(kDeviceID); + ASSERT_NE(nullptr, dps_ctx); + plgd_dps_set_configuration_resource(dps_ctx, true); + + ASSERT_TRUE(oc::SetAccessInRFOTM(dps_ctx->conf, true, + OC_PERM_RETRIEVE | OC_PERM_UPDATE)); + } + + static void SetUpTestCase() + { + EXPECT_TRUE(oc::TestDevice::StartServer()); + SetUpDPS(); + } + + static void TearDownTestCase() + { + plgd_dps_shutdown(); + oc::TestDevice::StopServer(); + } +}; + +template +static void +getRequestWithQuery(const std::string &query = "") +{ + auto epOpt = oc::TestDevice::GetEndpoint(kDeviceID); + ASSERT_TRUE(epOpt.has_value()); + auto ep = std::move(*epOpt); + + auto getHandler = [](oc_client_response_t *data) { + oc::TestDevice::Terminate(); + *static_cast(data->user_data) = true; + EXPECT_EQ(CODE, data->code); + DPS_DBG("GET payload: %s", + oc::RepPool::GetJson(data->payload, true).data()); + if (data->code != OC_STATUS_OK) { + return; + } + DPSData dpsData{}; + EXPECT_TRUE(parseDPSData(data->payload, dpsData)); + ASSERT_TRUE(dpsData.lastErrorCode.has_value()); + EXPECT_EQ(0, *dpsData.lastErrorCode); + ASSERT_TRUE(dpsData.provisionStatus.has_value()); + EXPECT_STREQ(kPlgdDpsStatusUninitialized, dpsData.provisionStatus->c_str()); + EXPECT_FALSE(dpsData.forceReprovision); + }; + + auto timeout = 1s; + bool invoked = false; + ASSERT_TRUE(oc_do_get_with_timeout( + PLGD_DPS_URI, &ep, query.empty() ? nullptr : query.c_str(), timeout.count(), + getHandler, HIGH_QOS, &invoked)); + oc::TestDevice::PoolEventsMsV1(timeout, true); + EXPECT_TRUE(invoked); +} + +TEST_F(DPSResourceTestWithServer, GetRequest_FailNoDPS) +{ + plgd_dps_shutdown(); + oc::TestDevice::PoolEventsMsV1(10ms); + + oc_resource_t *dps = dps_create_dpsconf_resource(kDeviceID); + ASSERT_NE(nullptr, dps); + ASSERT_TRUE(oc::SetAccessInRFOTM(dps, true, OC_PERM_RETRIEVE)); + + getRequestWithQuery(); + + dps_delete_dpsconf_resource(dps); + SetUpDPS(); +} + +TEST_F(DPSResourceTestWithServer, GetRequest_FailNoDPSResource) +{ + plgd_dps_context_t *dps_ctx = plgd_dps_get_context(kDeviceID); + ASSERT_NE(nullptr, dps_ctx); + plgd_dps_set_configuration_resource(dps_ctx, false); + + getRequestWithQuery(); + + plgd_dps_set_configuration_resource(dps_ctx, true); + ASSERT_TRUE(oc::SetAccessInRFOTM(dps_ctx->conf, true, + OC_PERM_RETRIEVE | OC_PERM_UPDATE)); +} + +TEST_F(DPSResourceTestWithServer, GetRequest) +{ + getRequestWithQuery("if=oic.if.r"); +} + +TEST_F(DPSResourceTestWithServer, GetRequestBaseline) +{ + getRequestWithQuery("if=oic.if.baseline"); +} + +template +static void +postRequest(Fn encode) +{ + auto epOpt = oc::TestDevice::GetEndpoint(kDeviceID); + ASSERT_TRUE(epOpt.has_value()); + auto ep = std::move(*epOpt); + + auto postHandler = [](oc_client_response_t *data) { + oc::TestDevice::Terminate(); + *static_cast(data->user_data) = true; + DPS_DBG("POST payload: %s", + oc::RepPool::GetJson(data->payload, true).data()); + EXPECT_EQ(OC_STATUS_CHANGED, data->code); + }; + + bool invoked = false; + ASSERT_TRUE( + oc_init_post(PLGD_DPS_URI, &ep, nullptr, postHandler, HIGH_QOS, &invoked)); + + encode(); + + auto timeout = 1s; + ASSERT_TRUE(oc_do_post_with_timeout(timeout.count())); + oc::TestDevice::PoolEventsMsV1(timeout, true); + EXPECT_TRUE(invoked); +} + +TEST_F(DPSResourceTestWithServer, PostRequest) +{ + std::string ep1URI = "coap://dps.plgd.dev"; + std::string ep1Name = "plgd.dev"; + std::string ep2URI = "coaps://dps.plgd.dev"; + std::string ep2Name = "plgd.dev2"; + std::string ep3URI = "coap+tcp://dps.plgd.dev"; + + postRequest([&ep1URI, &ep1Name, &ep2URI, &ep2Name, &ep3URI] { + oc_rep_begin_root_object(); + oc_rep_set_text_string(root, endpoint, ep1URI.c_str()); + oc_rep_set_text_string(root, endpointName, ep1Name.c_str()); + std::string_view key{ "endpoints" }; + g_err |= + oc_rep_encode_text_string(oc_rep_object(root), key.data(), key.length()); + oc_rep_begin_array(oc_rep_object(root), servers); + oc_rep_object_array_begin_item(servers); + oc_rep_set_text_string(servers, uri, ep2URI.c_str()); + oc_rep_set_text_string(servers, name, ep2Name.c_str()); + oc_rep_object_array_end_item(servers); + oc_rep_object_array_begin_item(servers); + oc_rep_set_text_string(servers, uri, ep3URI.c_str()); + oc_rep_object_array_end_item(servers); + oc_rep_end_array(oc_rep_object(root), servers); + oc_rep_end_root_object(); + }); + + plgd_dps_context_t *dps_ctx = plgd_dps_get_context(kDeviceID); + ASSERT_NE(nullptr, dps_ctx); + const auto *selected = plgd_dps_selected_endpoint_address(dps_ctx); + ASSERT_NE(nullptr, selected); + EXPECT_STREQ(ep1URI.c_str(), oc_string(*oc_endpoint_address_uri(selected))); + EXPECT_STREQ(ep1Name.c_str(), oc_string(*oc_endpoint_address_name(selected))); + + std::map> endpoints{}; + plgd_dps_iterate_server_addresses( + dps_ctx, + [](oc_endpoint_address_t *ea, void *data) { + auto &eps = + *static_cast *>( + data); + const oc_string_t *uri = oc_endpoint_address_uri(ea); + eps[oc_string(*uri)] = ea; + return true; + }, + &endpoints); + + ASSERT_EQ(3, endpoints.size()); + auto it = endpoints.find(ep1URI); + ASSERT_NE(endpoints.end(), it); + EXPECT_STREQ(ep1Name.c_str(), + oc_string(*oc_endpoint_address_name(it->second))); + it = endpoints.find(ep2URI); + ASSERT_NE(endpoints.end(), it); + EXPECT_STREQ(ep2Name.c_str(), + oc_string(*oc_endpoint_address_name(it->second))); + it = endpoints.find(ep3URI); + ASSERT_NE(endpoints.end(), it); + EXPECT_EQ(nullptr, oc_string(*oc_endpoint_address_name(it->second))); + + oc_endpoint_addresses_deinit(&dps_ctx->store.endpoints); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING && \ + OC_HAS_FEATURE_RESOURCE_ACCESS_IN_RFOTM */ diff --git a/api/plgd/unittest/plgd_dps_retry.cpp b/api/plgd/unittest/plgd_dps_retry.cpp new file mode 100644 index 000000000..563f41e9d --- /dev/null +++ b/api/plgd/unittest/plgd_dps_retry.cpp @@ -0,0 +1,92 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/plgd/device-provisioning-client/plgd_dps_context_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_retry_internal.h" + +#include "gtest/gtest.h" + +#include "oc_api.h" + +class TestDPSRetry : public testing::Test { +private: + static void SignalEventLoop() + { + // no-op for tests + } + + static int AppInit() + { + // no-op for tests + return 0; + } + +public: + void SetUp() override + { + static oc_handler_t handler{}; + handler.init = AppInit; + handler.signal_event_loop = SignalEventLoop; + EXPECT_EQ(0, oc_main_init(&handler)); + } + + void TearDown() override { oc_main_shutdown(); } +}; + +TEST_F(TestDPSRetry, IncrementRetry) +{ + plgd_dps_context_t ctx; + memset(&ctx, 0, sizeof(ctx)); + dps_retry_init(&ctx.retry); + + size_t size = dps_retry_size(&ctx.retry); + EXPECT_LE(2, size); + for (size_t i = 0; i < size - 1; i++) { + dps_retry_increment(&ctx, PLGD_DPS_GET_CREDENTIALS); + EXPECT_LT(0, ctx.retry.count); + } + dps_retry_increment(&ctx, PLGD_DPS_GET_CREDENTIALS); + EXPECT_EQ(0, ctx.retry.count); +} + +TEST_F(TestDPSRetry, ResetRetry) +{ + plgd_dps_context_t ctx; + memset(&ctx, 0, sizeof(ctx)); + dps_retry_init(&ctx.retry); + EXPECT_EQ(0, ctx.retry.count); + + dps_retry_increment(&ctx, PLGD_DPS_GET_CREDENTIALS); + EXPECT_LT(0, ctx.retry.count); + + dps_retry_reset(&ctx, PLGD_DPS_GET_CREDENTIALS); + EXPECT_EQ(0, ctx.retry.count); + + // change default_cfg size 0 to invoke reset internally + ctx.retry.default_cfg[0] = 0; + dps_retry_increment(&ctx, PLGD_DPS_GET_CREDENTIALS); + EXPECT_EQ(0, ctx.retry.count); + EXPECT_EQ(DEFAULT_RESET_TIMEOUT, ctx.retry.schedule_action.timeout); + EXPECT_LT(0, ctx.retry.schedule_action.delay); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_security.cpp b/api/plgd/unittest/plgd_dps_security.cpp new file mode 100644 index 000000000..a221ee32c --- /dev/null +++ b/api/plgd/unittest/plgd_dps_security.cpp @@ -0,0 +1,81 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/plgd/device-provisioning-client/plgd_dps_pki_internal.h" + +#include "gtest/gtest.h" + +#include + +TEST(DPSSecurityTest, CalculateCertificateCheckInterval) +{ + time_t now = time(nullptr); + EXPECT_NE(-1, now); + + const uint16_t expiresIn = 60; + dps_pki_configuration_t cfg = { /*.expiring_limit =*/expiresIn }; + uint64_t expired{ static_cast(now) - 1 }; + uint64_t expiring{ static_cast(now) + (expiresIn / 2) }; + // expiring and expired certificates should be immediately renewed + EXPECT_EQ(0, dps_pki_calculate_renew_certificates_interval(cfg, expired)); + EXPECT_EQ(0, dps_pki_calculate_renew_certificates_interval(cfg, expiring)); + + // non-expiring certificates should have some non-zero time for renewal + uint64_t valid{ static_cast(now) + (expiresIn * 2) }; + EXPECT_LT(0, dps_pki_calculate_renew_certificates_interval(cfg, valid)); +} + +TEST(DPSSecurityTest, CertificateStateToStr) +{ + EXPECT_STREQ("valid", + dps_pki_certificate_state_to_str(DPS_CERTIFICATE_VALID)); + EXPECT_STREQ("expired", + dps_pki_certificate_state_to_str(DPS_CERTIFICATE_EXPIRED)); +} + +TEST(DPSSecurityTest, CertificateValidityToState) +{ + time_t now = time(nullptr); + EXPECT_NE(-1, now); + + dps_pki_configuration_t cfg = { + /*.expiring_limit = */ 60, + }; + + time_t future = now + 60; + EXPECT_EQ(DPS_CERTIFICATE_NOT_YET_VALID, + dps_pki_validate_certificate(cfg, future, 0)); + + time_t expiring = now + cfg.expiring_limit - 1; + EXPECT_EQ(DPS_CERTIFICATE_EXPIRING, + dps_pki_validate_certificate(cfg, now, expiring)); + + time_t past = now - 1; + EXPECT_EQ(DPS_CERTIFICATE_EXPIRED, + dps_pki_validate_certificate(cfg, past, past)); + + time_t valid = now + cfg.expiring_limit + 60; + EXPECT_EQ(DPS_CERTIFICATE_VALID, + dps_pki_validate_certificate(cfg, now, valid)); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_store.cpp b/api/plgd/unittest/plgd_dps_store.cpp new file mode 100644 index 000000000..9c00a06f2 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_store.cpp @@ -0,0 +1,313 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/oc_helpers_internal.h" +#include "api/oc_runtime_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_log_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_store_internal.h" +#include "oc_helpers.h" +#include "plgd_dps_test.h" +#include "security/oc_pstat_internal.h" +#include "tests/gtest/Device.h" +#include "tests/gtest/RepPool.h" +#include "util/oc_endpoint_address_internal.h" + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +static constexpr size_t kDeviceID = 0; +static constexpr std::string_view kStoragePath{ "dps_test_storage" }; + +using namespace std::chrono_literals; + +class DPSStoreTest : public testing::Test { +public: +public: + static void SetUpTestCase() { oc_runtime_init(); } + + static void TearDownTestCase() { oc_runtime_shutdown(); } + + static bool is_directory(const char *path) + { + struct stat statbuf; + if (stat(path, &statbuf) != 0) { + return false; + } + return S_ISDIR(statbuf.st_mode) != 0; + } + + static bool make_storage(std::string_view storage) + { + if (mkdir(storage.data(), S_IRWXU | S_IRWXG) == 0) { + return true; + } + if ((EEXIST != errno) || !is_directory(storage.data())) { + DPS_ERR("failed to create storage at path %s", storage.data()); + return false; + } + return true; + } + + static void clean_storage() + { + for (const auto &entry : + std::filesystem::directory_iterator(kStoragePath.data())) { + std::filesystem::remove_all(entry.path()); + } + } +}; + +static void +compareStores(const plgd_dps_store_t &store1, const plgd_dps_store_t &store2) +{ + ASSERT_EQ(oc_endpoint_addresses_size(&store1.endpoints), + oc_endpoint_addresses_size(&store2.endpoints)); + const char *owner1 = oc_string(store1.owner); + const char *owner2 = oc_string(store2.owner); + if (owner1 == nullptr) { + ASSERT_EQ(nullptr, owner2); + } else { + ASSERT_NE(nullptr, owner2); + EXPECT_STREQ(owner1, owner2); + } + EXPECT_EQ(store1.has_been_provisioned_since_reset, + store2.has_been_provisioned_since_reset); +} + +TEST_F(DPSStoreTest, EncodeAndDecode) +{ + auto ctx = dps::make_unique_context(kDeviceID); + + std::string ep1_uri = "/uri/1"; + std::string ep1_name = "name1"; + auto *ep1 = + plgd_dps_add_endpoint_address(ctx.get(), ep1_uri.c_str(), ep1_uri.length(), + ep1_name.c_str(), ep1_name.length()); + ASSERT_NE(nullptr, ep1); + std::string ep2_uri = "/uri/2"; + std::string ep2_name = "name2"; + auto *ep2 = + plgd_dps_add_endpoint_address(ctx.get(), ep2_uri.c_str(), ep2_uri.length(), + ep2_name.c_str(), ep2_name.length()); + ASSERT_NE(nullptr, ep2); + + oc::RepPool pool{}; + ASSERT_TRUE(dps_store_encode(&ctx->store)); + + plgd_dps_store_t store{}; + dps_store_init(&store, nullptr, nullptr); + auto rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + dps_store_decode(rep.get(), &store); + + compareStores(ctx->store, store); + + dps_store_deinit(&store); +} + +TEST_F(DPSStoreTest, Decode_SelectedEndpointMissingURI) +{ + oc::RepPool pool{}; + + oc_rep_begin_root_object(); + oc_rep_set_text_string_v1(root, ep, "", 0); + oc_rep_end_root_object(); + ASSERT_EQ(CborNoError, oc_rep_get_cbor_errno()); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + + plgd_dps_store_t store{}; + dps_store_init(&store, nullptr, nullptr); + dps_store_decode(rep.get(), &store); + EXPECT_EQ(0, oc_endpoint_addresses_size(&store.endpoints)); + + dps_store_deinit(&store); +} + +TEST_F(DPSStoreTest, Decode_EndpointMissingURI) +{ + oc::RepPool pool{}; + + oc_rep_begin_root_object(); + oc_rep_open_array(root, eps); + oc_rep_object_array_begin_item(eps); + oc_rep_set_text_string_v1(eps, ep, "", 0); + oc_rep_object_array_end_item(eps); + oc_rep_close_array(root, eps); + oc_rep_end_root_object(); + ASSERT_EQ(CborNoError, oc_rep_get_cbor_errno()); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + + plgd_dps_store_t store{}; + dps_store_init(&store, nullptr, nullptr); + dps_store_decode(rep.get(), &store); + EXPECT_EQ(0, oc_endpoint_addresses_size(&store.endpoints)); + + dps_store_deinit(&store); +} + +TEST_F(DPSStoreTest, SaveAndLoad) +{ + auto ctx = dps::make_unique_context(kDeviceID); + + EXPECT_GT(0, dps_store_load(&ctx->store, kDeviceID)); + EXPECT_GT(0, dps_store_dump(&ctx->store, kDeviceID)); + + EXPECT_FALSE(ctx->store.has_been_provisioned_since_reset); + + ASSERT_TRUE(make_storage(kStoragePath)); + ASSERT_EQ(0, oc_storage_config(kStoragePath.data())); + std::string endpoint_addr = "coaps+tcp://127.0.0.1:12345"; + std::string endpoint_name = "dps test endpoint"; + ASSERT_NE(nullptr, plgd_dps_add_endpoint_address( + ctx.get(), endpoint_addr.c_str(), endpoint_addr.length(), + endpoint_name.c_str(), endpoint_name.length())); + ctx->store.has_been_provisioned_since_reset = true; + auto *selected = plgd_dps_selected_endpoint_address(ctx.get()); + ASSERT_NE(nullptr, selected); + auto *selected_addr = oc_endpoint_address_uri(selected); + ASSERT_NE(nullptr, selected_addr); + EXPECT_STREQ(endpoint_addr.c_str(), oc_string(*selected_addr)); + auto *selected_name = oc_endpoint_address_name(selected); + ASSERT_NE(nullptr, selected_name); + EXPECT_STREQ(endpoint_name.c_str(), oc_string(*selected_name)); + ASSERT_EQ(0, dps_store_dump(&ctx->store, kDeviceID)); + + oc_endpoint_addresses_deinit(&ctx->store.endpoints); + selected = plgd_dps_selected_endpoint_address(ctx.get()); + ASSERT_EQ(nullptr, selected); + ctx->store.has_been_provisioned_since_reset = false; + + EXPECT_EQ(0, dps_store_load(&ctx->store, kDeviceID)); + selected = plgd_dps_selected_endpoint_address(ctx.get()); + ASSERT_NE(nullptr, selected); + selected_addr = oc_endpoint_address_uri(selected); + ASSERT_NE(nullptr, selected_addr); + EXPECT_STREQ(endpoint_addr.c_str(), oc_string(*selected_addr)); + selected_name = oc_endpoint_address_name(selected); + ASSERT_NE(nullptr, selected_name); + EXPECT_STREQ(endpoint_name.c_str(), oc_string(*selected_name)); + EXPECT_TRUE(ctx->store.has_been_provisioned_since_reset); + + clean_storage(); +} + +class DPSStoreWithDeviceTest : public testing::Test { +public: + static void SetUpTestCase() + { + ASSERT_TRUE(oc::TestDevice::StartServer()); + + oc_sec_pstat_t *pstat = oc_sec_get_pstat(kDeviceID); + pstat->s = OC_DOS_RFNOP; + plgd_dps_init(); + } + + static void TearDownTestCase() + { + plgd_dps_shutdown(); + oc::TestDevice::StopServer(); + } + + void SetUp() override + { + ASSERT_TRUE(DPSStoreTest::make_storage(kStoragePath)); + ASSERT_EQ(0, oc_storage_config(kStoragePath.data())); + } + + void TearDown() override { DPSStoreTest::clean_storage(); } +}; + +TEST_F(DPSStoreWithDeviceTest, DumpOnEndpointChange) +{ + auto *ctx = plgd_dps_get_context(kDeviceID); + ASSERT_NE(nullptr, ctx); + + plgd_dps_store_t store{}; + dps_store_init(&store, nullptr, nullptr); + ASSERT_GT(0, dps_store_load(&store, kDeviceID)); + + std::string ep1_uri = "coap+tcp://127.0.0.1:12345"; + ASSERT_NE(nullptr, plgd_dps_add_endpoint_address( + ctx, ep1_uri.c_str(), ep1_uri.length(), nullptr, 0)); + std::string ep2_uri = "coap://127.0.0.1:12345"; + ASSERT_NE(nullptr, plgd_dps_add_endpoint_address( + ctx, ep2_uri.c_str(), ep2_uri.length(), nullptr, 0)); +#ifdef OC_DYNAMIC_ALLOCATION + std::string ep3_uri = "coaps+tcp://127.0.0.1:12345"; + ASSERT_NE(nullptr, plgd_dps_add_endpoint_address( + ctx, ep3_uri.c_str(), ep3_uri.length(), nullptr, 0)); +#endif /* OC_DYNAMIC_ALLOCATION */ + oc::TestDevice::PoolEventsMsV1(10ms); + ASSERT_EQ(0, dps_store_load(&store, kDeviceID)); +#ifdef OC_DYNAMIC_ALLOCATION + EXPECT_EQ(3, oc_endpoint_addresses_size(&store.endpoints)); +#else /* !OC_DYNAMIC_ALLOCATION */ + EXPECT_EQ(2, oc_endpoint_addresses_size(&store.endpoints)); +#endif /* OC_DYNAMIC_ALLOCATION */ + EXPECT_TRUE(oc_endpoint_addresses_is_selected( + &store.endpoints, oc_string_view(ep1_uri.c_str(), ep1_uri.length()))); + + // change selected endpoint + oc_endpoint_addresses_select_by_uri( + &ctx->store.endpoints, oc_string_view(ep2_uri.c_str(), ep2_uri.length())); + oc::TestDevice::PoolEventsMsV1(10ms); + dps_store_init(&store, nullptr, nullptr); + ASSERT_EQ(0, dps_store_load(&store, kDeviceID)); +#ifdef OC_DYNAMIC_ALLOCATION + EXPECT_EQ(3, oc_endpoint_addresses_size(&store.endpoints)); +#else /* !OC_DYNAMIC_ALLOCATION */ + EXPECT_EQ(2, oc_endpoint_addresses_size(&store.endpoints)); +#endif /* OC_DYNAMIC_ALLOCATION */ + EXPECT_TRUE(oc_endpoint_addresses_is_selected( + &store.endpoints, oc_string_view(ep2_uri.c_str(), ep2_uri.length()))); + + // remove selected endpoint + ASSERT_TRUE(oc_endpoint_addresses_remove_by_uri( + &ctx->store.endpoints, oc_string_view(ep2_uri.c_str(), ep2_uri.length()))); +#ifdef OC_DYNAMIC_ALLOCATION + oc_string_view_t selected = oc_string_view(ep3_uri.c_str(), ep3_uri.length()); +#else /* !OC_DYNAMIC_ALLOCATION */ + oc_string_view_t selected = oc_string_view(ep1_uri.c_str(), ep1_uri.length()); +#endif /* OC_DYNAMIC_ALLOCATION */ + ASSERT_TRUE( + oc_endpoint_addresses_is_selected(&ctx->store.endpoints, selected)); + oc::TestDevice::PoolEventsMsV1(10ms); + dps_store_init(&store, nullptr, nullptr); + ASSERT_EQ(0, dps_store_load(&store, kDeviceID)); +#ifdef OC_DYNAMIC_ALLOCATION + EXPECT_EQ(2, oc_endpoint_addresses_size(&store.endpoints)); +#else /* !OC_DYNAMIC_ALLOCATION */ + EXPECT_EQ(1, oc_endpoint_addresses_size(&store.endpoints)); +#endif /* OC_DYNAMIC_ALLOCATION */ + EXPECT_TRUE(oc_endpoint_addresses_is_selected(&store.endpoints, selected)); + + dps_store_deinit(&store); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_tag.cpp b/api/plgd/unittest/plgd_dps_tag.cpp new file mode 100644 index 000000000..eba5a77b1 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_tag.cpp @@ -0,0 +1,229 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/plgd/device-provisioning-client/plgd_dps_tag_internal.h" +#include "oc_acl.h" +#include "oc_api.h" +#include "oc_cred.h" +#include "oc_pki.h" + +#include "gtest/gtest.h" + +#include +#include + +class TestDPSTag : public testing::Test { +private: + static void SignalEventLoop() + { + // no-op for tests + } + + static int AppInit() + { + if (oc_init_platform("Samsung", nullptr, nullptr) != 0) { + return -1; + } + if (oc_add_device("/oic/d", "oic.d.light", "Lamp", "ocf.1.0.0", + "ocf.res.1.0.0", nullptr, nullptr) != 0) { + return -1; + } + return 0; + } + +public: + void SetUp() override + { + static oc_handler_t handler{}; + handler.init = AppInit; + handler.signal_event_loop = SignalEventLoop; + EXPECT_EQ(0, oc_main_init(&handler)); + } + + void TearDown() override { oc_main_shutdown(); } +}; + +#ifdef OC_DYNAMIC_ALLOCATION + +static bool +is_directory(const std::string &path) +{ + struct stat statbuf; + if (stat(path.c_str(), &statbuf) != 0) { + return false; + } + return S_ISDIR(statbuf.st_mode) != 0; +} + +static int +read_pem(const std::string &file_path, std::vector &buffer) +{ + FILE *file = fopen(file_path.c_str(), "r"); + if (file == nullptr) { + return -1; + } + if (fseek(file, 0, SEEK_END) != 0) { + fclose(file); + return -1; + } + long pem_len = ftell(file); + if (pem_len < 0) { + fclose(file); + return -1; + } + if (fseek(file, 0, SEEK_SET) != 0) { + fclose(file); + return -1; + } + buffer.resize(pem_len); + if (fread(&buffer[0], 1, pem_len, file) < (size_t)pem_len) { + fclose(file); + return -1; + } + fclose(file); + buffer.push_back('\0'); + return 0; +} + +static int +add_mfg_certificate(size_t device, const std::string &cert_dir) +{ + int mfg_credid = -1; + std::string path = cert_dir + "/ee.pem"; + std::vector mfg_crt = {}; + std::vector mfg_key = {}; + oc_sec_cred_t *cred; + if (read_pem(path, mfg_crt) < 0) { + goto error; + } + path = cert_dir + "/key.pem"; + if (read_pem(path, mfg_key) < 0) { + goto error; + } + mfg_credid = oc_pki_add_mfg_cert(device, mfg_crt.data(), mfg_crt.size(), + mfg_key.data(), mfg_key.size()); + if (mfg_credid < 0) { + goto error; + } + + cred = oc_sec_get_cred_by_credid(mfg_credid, device); + if (cred == nullptr) { + goto error; + } + + oc_set_string(&cred->tag, DPS_TAG, DPS_TAG_LEN); + return mfg_credid; +error: + if (mfg_credid != -1) { + oc_sec_remove_cred_by_credid(mfg_credid, device); + } + return -1; +} + +TEST_F(TestDPSTag, TagCredentials) +{ + std::string cert_dir = "pki_certs"; + if (!is_directory(cert_dir)) { + return; + } + + int mfg_credid = add_mfg_certificate(0, cert_dir); + ASSERT_LT(0, mfg_credid); + + dps_credentials_set_stale_tag(0); + oc_sec_cred_t *cred = oc_sec_get_cred_by_credid(mfg_credid, 0); + ASSERT_NE(nullptr, cred); + EXPECT_STREQ(DPS_STALE_TAG, oc_string(cred->tag)); + + dps_credentials_remove_stale_tag(0); + cred = oc_sec_get_cred_by_credid(mfg_credid, 0); + ASSERT_NE(nullptr, cred); + EXPECT_STREQ(DPS_TAG, oc_string(cred->tag)); + + oc_sec_remove_cred_by_credid(mfg_credid, 0); +} + +#endif /* OC_DYNAMIC_ALLOCATION */ + +TEST_F(TestDPSTag, TagNoCredentials) +{ + oc_sec_creds_t *creds = oc_sec_get_creds(0); + ASSERT_NE(nullptr, creds); + + for (const auto *cred = + static_cast(oc_list_head(creds->creds)); + cred != nullptr; cred = cred->next) { + ASSERT_EQ(nullptr, oc_string(cred->tag)); + } + dps_credentials_set_stale_tag(0); + + for (const auto *cred = + static_cast(oc_list_head(creds->creds)); + cred != nullptr; cred = cred->next) { + EXPECT_EQ(nullptr, oc_string(cred->tag)); + } + + dps_credentials_remove_stale_tag(0); +} + +TEST_F(TestDPSTag, TagACLs) +{ + EXPECT_TRUE(oc_sec_acl_add_bootstrap_acl(0)); + auto *ace = (oc_sec_ace_t *)oc_list_head(oc_sec_get_acl(0)->subjects); + EXPECT_NE(nullptr, ace); + oc_set_string(&ace->tag, DPS_TAG, DPS_TAG_LEN); + + dps_acls_set_stale_tag(0); + ace = (oc_sec_ace_t *)oc_list_head(oc_sec_get_acl(0)->subjects); + ASSERT_NE(nullptr, ace); + EXPECT_STREQ(DPS_STALE_TAG, oc_string(ace->tag)); + + dps_acls_remove_stale_tag(0); + ace = (oc_sec_ace_t *)oc_list_head(oc_sec_get_acl(0)->subjects); + ASSERT_NE(nullptr, ace); + EXPECT_STREQ(DPS_TAG, oc_string(ace->tag)); + + oc_set_string(&ace->tag, nullptr, 0); +} + +TEST_F(TestDPSTag, TagNoACLs) +{ + oc_sec_acl_t *acl = oc_sec_get_acl(0); + ASSERT_NE(nullptr, acl); + + for (const auto *ace = + static_cast(oc_list_head(acl->subjects)); + ace != nullptr; ace = ace->next) { + ASSERT_EQ(nullptr, oc_string(ace->tag)); + } + + dps_acls_set_stale_tag(0); + for (const auto *ace = + static_cast(oc_list_head(acl->subjects)); + ace != nullptr; ace = ace->next) { + EXPECT_EQ(nullptr, oc_string(ace->tag)); + } + + dps_acls_remove_stale_tag(0); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */