diff --git a/.github/workflows/L1-tests.yml b/.github/workflows/L1-tests.yml new file mode 100644 index 00000000..bc17965c --- /dev/null +++ b/.github/workflows/L1-tests.yml @@ -0,0 +1,252 @@ +name: L1-tests + +on: + push: + branches: + - develop + - main + pull_request: + branches: + - develop + - main + workflow_call: + secrets: + RDKCM_RDKE: + required: true + workflow_dispatch: + +permissions: + contents: read + +env: + BUILD_TYPE: Debug + +jobs: + L1-tests: + name: Build and run unit tests + runs-on: ubuntu-22.04 + + steps: + - name: Set up CMake + uses: jwlawson/actions-setup-cmake@v1.13 + with: + cmake-version: '3.16.x' + github-api-token: '' + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - run: pip install jsonref + + - name: ACK External Trigger + run: | + echo "Message: External Trigger Received for L1 Tests" + echo "Trigger Source: ${{ inputs.caller_source }}" + + - name: Set up CMake + uses: jwlawson/actions-setup-cmake@v1.13 + with: + cmake-version: '3.16.x' + + - name: Install packages + run: > + sudo apt update + && + sudo apt install -y + autoconf automake libtool pkg-config + libgtest-dev libgmock-dev + build-essential g++ cmake + valgrind lcov clang ninja-build + libglib2.0-dev libdbus-1-dev + libgssdp-1.2-dev libsoup2.4-dev libxml2-dev + uuid-dev libcurl4-openssl-dev + libsystemd-dev libboost-all-dev libwebsocketpp-dev + meson libcunit1 libcunit1-dev curl + protobuf-compiler-grpc libgrpc-dev libgrpc++-dev + libyajl-dev + + - name: Build trower-base64 + run: | + if [ ! -d "trower-base64" ]; then + git clone https://github.com/xmidt-org/trower-base64.git + fi + cd trower-base64 + meson setup --warnlevel 3 --werror build + ninja -C build + sudo ninja -C build install + + - name: Checkout xdialserver + uses: actions/checkout@v3 + with: + path: xdialserver + + # - name: Checkout entservices-testframework + # uses: actions/checkout@v3 + # with: + # repository: rdkcentral/entservices-testframework + # path: entservices-testframework + # token: ${{ secrets.RDKCM_RDKE }} + # ref: feature/RDKEMW-14961-2 + + - name: Checkout googletest + uses: actions/checkout@v3 + with: + repository: google/googletest + path: googletest + ref: v1.15.0 + + - name: Build googletest + run: | + cmake -S "$GITHUB_WORKSPACE/googletest" \ + -B build/googletest \ + -DCMAKE_INSTALL_PREFIX="$GITHUB_WORKSPACE/install/usr" \ + -DBUILD_GMOCK=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON + cmake --build build/googletest -j$(nproc) + cmake --install build/googletest + + # - name: Generate external headers + # # Empty headers to mute errors + # run: > + # cd "$GITHUB_WORKSPACE/entservices-testframework/Tests/" + # && + # mkdir -p + # headers + # headers/WPEFramework + # headers/WPEFramework/core + # headers/WPEFramework/interfaces + # headers/rdk/iarmbus + # headers/rdk/iarmmgrs-hal + # headers/uuid + # && + # cd headers + # && + # touch + # WPEFramework/core.h + # WPEFramework/interfaces.h + # WPEFramework/plugins.h + # rdk/iarmbus/libIBus.h + # rdk/iarmbus/libIBusDaemon.h + # rdk/iarmmgrs-hal/iarmmgrs_hal.h + # uuid/uuid.h + # && + # mkdir -p proc + # && + # touch proc/readproc.h + + - name: Generate stub headers + # Individual shadow headers that source files #include are generated + # here as thin wrappers so they never need to be committed. + # Truly empty stubs are created with touch. + # C++ shim definitions for gdial.cpp live in + # tests/L1Tests/stubs/gdial_cpp_test_stubs.hpp and are included by + # lightweight wrappers generated here. + run: | + STUBS="$GITHUB_WORKSPACE/xdialserver/tests/L1Tests/stubs" + WRAPPER='#pragma once' + CPP_WRAPPER='#pragma once\n#include "gdial_cpp_test_stubs.hpp"' + + # Create stubs directory if it doesn't exist + mkdir -p "$STUBS" + + # Shadow headers: block the real headers and redirect to the + # combined stub. + for f in \ + libIBus.h libIARMCore.h; do + printf "$WRAPPER\n" > "$STUBS/$f" + done + + # Empty stubs: headers from WPEFramework and IARM that are unused + # in the test paths. + touch \ + "$STUBS/libIARMBus.h" \ + "$STUBS/libIBusDaemon.h" \ + "$STUBS/libIARMUtil.h" + + # gdial.cpp wrappers: keep source definitions in a committed mock + # file and generate only include shims here. + mkdir -p "$STUBS/com" "$STUBS/json" "$STUBS/securityagent" + for f in core.h plugins.h securityagent/SecurityTokenUtil.h; do + printf "$CPP_WRAPPER\n" > "$STUBS/$f" + done + + # Truly empty stubs for unused headers. + touch \ + "$STUBS/com/Ids.h" \ + "$STUBS/json/JsonData_Netflix.h" \ + "$STUBS/json/JsonData_StateControl.h" + + - name: Build xdialserver L1 tests + run: | + cd "$GITHUB_WORKSPACE/xdialserver" + autoreconf -if + + # Compiler / linker flags for coverage instrumentation + export CFLAGS="-fprofile-arcs -ftest-coverage -g -O0" + export CXXFLAGS="-fprofile-arcs -ftest-coverage -g -O0" + export LDFLAGS="--coverage" + + # Tell pkg-config / the build where to find googletest + export PKG_CONFIG_PATH="$GITHUB_WORKSPACE/install/usr/lib/pkgconfig:${PKG_CONFIG_PATH:-}" + export CPPFLAGS="-I$GITHUB_WORKSPACE/install/usr/include" + export LIBRARY_PATH="$GITHUB_WORKSPACE/install/usr/lib:${LIBRARY_PATH:-}" + + ./configure --enable-l1tests + # TESTFRAMEWORK_DIR="$GITHUB_WORKSPACE/entservices-testframework" + make -C tests/L1Tests + + - name: Run unit tests without valgrind + run: | + cd "$GITHUB_WORKSPACE/xdialserver/tests/L1Tests" + ./run_L1Tests --gtest_output="json:$GITHUB_WORKSPACE/xdialserverL1TestResults.json" + cp "$GITHUB_WORKSPACE/xdialserverL1TestResults.json" \ + "$GITHUB_WORKSPACE/xdialserverL1TestResultsWithoutValgrind.json" + + - name: Run unit tests with valgrind + if: ${{ !env.ACT }} + run: | + cd "$GITHUB_WORKSPACE/xdialserver/tests/L1Tests" + valgrind \ + --tool=memcheck \ + --log-file="$GITHUB_WORKSPACE/valgrind_log" \ + --leak-check=yes \ + --show-reachable=yes \ + --track-fds=yes \ + --fair-sched=try \ + ./run_L1Tests --gtest_output="json:$GITHUB_WORKSPACE/xdialserverL1TestResultsWithValgrind.json" + + - name: Generate coverage + if: ${{ !env.ACT }} + run: | + lcov -c \ + -o coverage.info \ + -d "$GITHUB_WORKSPACE/xdialserver" + + lcov -r coverage.info \ + '/usr/include/*' \ + '*/install/usr/include/*' \ + '*/googletest/*' \ + '*/entservices-testframework/*' \ + '*/tests/*' \ + '*/mocks/*' \ + '*/stubs/*' \ + -o filtered_coverage.info + + genhtml \ + -o coverage \ + -t "xdialserver L1 coverage" \ + filtered_coverage.info + + - name: Upload artifacts + if: ${{ !env.ACT }} + uses: actions/upload-artifact@v4 + with: + name: artifacts-L1-xdialserver + path: | + coverage/ + valgrind_log + xdialserverL1TestResultsWithoutValgrind.json + xdialserverL1TestResultsWithValgrind.json + if-no-files-found: warn diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 00000000..55aaab31 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,20 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2019 RDK Management +# +# 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. +########################################################################## + +SUBDIRS = tests diff --git a/configure.ac b/configure.ac new file mode 100644 index 00000000..88d83336 --- /dev/null +++ b/configure.ac @@ -0,0 +1,72 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2019 RDK Management +# +# 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. +########################################################################## + +AC_INIT([xdialserver], [1.0.0]) +AM_INIT_AUTOMAKE([-Wall -Werror foreign subdir-objects]) + +AC_PROG_CC +AC_PROG_CXX +AC_PROG_RANLIB +AM_PROG_AR + +# Check for pkg-config +PKG_CHECK_MODULES([GLIB], [glib-2.0]) +PKG_CHECK_MODULES([GIO], [gio-2.0]) + +# Check for DIAL protocol dependencies +PKG_CHECK_MODULES([GSSDP], [gssdp-1.2], + [], + [PKG_CHECK_MODULES([GSSDP], [gssdp-1.0], + [], + [AC_MSG_WARN([gssdp library not found])])]) + +PKG_CHECK_MODULES([SOUP], [libsoup-2.4], + [], + [PKG_CHECK_MODULES([SOUP], [libsoup-3.0], + [], + [AC_MSG_WARN([libsoup library not found])])]) + +PKG_CHECK_MODULES([XML2], [libxml-2.0]) + +# Optional RDK-specific packages +PKG_CHECK_MODULES([WPEFRAMEWORK], [WPEFramework], [], + [AC_MSG_WARN([WPEFramework development headers not found - platform tests may be limited])]) +PKG_CHECK_MODULES([IARMBUS], [libIARMBus], [], + [AC_MSG_WARN([libIARMBus development headers not found - IARM tests may be limited])]) + +# L1 Tests support +AC_ARG_ENABLE([l1tests], + [AS_HELP_STRING([--enable-l1tests], [Enable L1 unit tests])], + [enable_l1tests="$enableval"], + [enable_l1tests="no"]) + +AM_CONDITIONAL([ENABLE_L1TESTS], [test "x$enable_l1tests" = "xyes"]) + +if test "x$enable_l1tests" = "xyes"; then + PKG_CHECK_MODULES([GTEST], [gtest], [], [AC_MSG_WARN([gtest not found in pkg-config])]) + PKG_CHECK_MODULES([GMOCK], [gmock], [], [AC_MSG_WARN([gmock not found in pkg-config])]) +fi + +AC_CONFIG_FILES([ + Makefile + tests/Makefile + tests/L1Tests/Makefile +]) + +AC_OUTPUT diff --git a/tests/L1Tests/Makefile.am b/tests/L1Tests/Makefile.am new file mode 100644 index 00000000..c74d0e21 --- /dev/null +++ b/tests/L1Tests/Makefile.am @@ -0,0 +1,96 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2024 RDK Management +# +# 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 paths: +# - tests/L1Tests/stubs : shadow headers that redirect to test stubs +# - tests/L1Tests/mocks : shared mock implementations +# - server/include : xdialserver public API headers +# - server : xdialserver implementation headers +# - server/plat : platform-specific headers +# +# Note: WPEFramework and IARM bus headers are optional and mocked in the test +# environment. The test stubs provide mock implementations, so the actual +# development packages (libwpeframework-dev, libiarmbus-dev) are not required +# for basic testing. + +AM_CPPFLAGS = -I$(top_srcdir)/tests/L1Tests/stubs \ + -I$(top_srcdir)/tests/L1Tests/mocks \ + -I$(top_srcdir)/server/include \ + -I$(top_srcdir)/server \ + -I$(top_srcdir)/server/plat \ + -DGDIAL_STATIC=extern \ + -DGDIAL_STATIC_INLINE=extern \ + -D_GNU_SOURCE \ + -DHAVE_GSSDP_VERSION_1_2_OR_NEWER \ + $(GLIB_CFLAGS) \ + $(GIO_CFLAGS) \ + $(GSSDP_CFLAGS) \ + $(SOUP_CFLAGS) \ + $(XML2_CFLAGS) + +AM_CXXFLAGS = -Wall -std=c++14 -g + +AM_LDFLAGS = -pthread + +# ---------- Test binary ---------- +noinst_PROGRAMS = run_L1Tests + +# New test checklist: +# 1) Add new test_*.cpp to run_L1Tests_SOURCES below. +# 2) Keep tests in the matching folder (server/, plat/, utils/). +# 3) If new stubs are required, add them under tests/L1Tests/stubs and/or +# tests/L1Tests/mocks, and ensure CI wrapper generation covers them. +# 4) For modules with static/global state, add explicit fixture cleanup to +# avoid cross-test contamination in the single-process run_L1Tests binary. +run_L1Tests_SOURCES = \ + test_main.cpp \ + server/test_gdialServer.cpp \ + server/test_gdialService.cpp \ + server/test_gdialserver_ut.cpp \ + server/test_gdialRest.cpp \ + server/test_gdialShield.cpp \ + server/test_gdialSsdp.cpp \ + mocks/gssdp_mock.c \ + plat/test_gdialPlat.cpp \ + plat/test_gdialAppCache.cpp \ + plat/test_gdialCpp.cpp \ + plat/test_gdialPlatApp.cpp \ + plat/test_gdialPlatDev.cpp \ + plat/test_gdialPlatUtil.cpp \ + utils/test_gdialUtil.cpp \ + plat/gdial_os_stubs.cpp \ + $(top_srcdir)/server/gdialservice.cpp \ + $(top_srcdir)/server/gdial-util.c \ + $(top_srcdir)/server/gdial-app.c \ + $(top_srcdir)/server/gdial-rest.c \ + $(top_srcdir)/server/gdial-shield.c \ + $(top_srcdir)/server/gdial-ssdp.c \ + $(top_srcdir)/server/plat/gdial-plat-app.c \ + $(top_srcdir)/server/plat/gdial-plat-dev.c \ + $(top_srcdir)/server/plat/gdial-plat-util.c \ + $(top_srcdir)/server/plat/gdialappcache.cpp \ + $(top_srcdir)/server/plat/gdial_app_registry.c + +run_L1Tests_LDADD = -lgtest -lgmock $(GLIB_LIBS) $(GIO_LIBS) $(SOUP_LIBS) $(XML2_LIBS) -luuid + +# Run tests on 'make check' +TESTS = run_L1Tests + +# Clean up test artifacts +CLEANFILES = *.log *.trs diff --git a/tests/L1Tests/mocks/gssdp_mock.c b/tests/L1Tests/mocks/gssdp_mock.c new file mode 100644 index 00000000..c8406b64 --- /dev/null +++ b/tests/L1Tests/mocks/gssdp_mock.c @@ -0,0 +1,77 @@ +/* + * Minimal GSSDP mocks for L1 tests. + * These avoid runtime dependency on host network/GSSDP behavior while + * allowing gdial-ssdp.c code paths to execute for coverage. + */ + +#include +#include + +#ifndef HAVE_GSSDP_VERSION_1_2_OR_NEWER +GSSDPClient *gssdp_client_new(GMainContext *main_context, const char *iface, GError **error) +{ + (void)main_context; + (void)iface; + if (error) { + *error = NULL; + } + return (GSSDPClient *)g_object_new(G_TYPE_OBJECT, NULL); +} +#else +GSSDPClient *gssdp_client_new(const char *iface, GError **error) +{ + (void)iface; + if (error) { + *error = NULL; + } + return (GSSDPClient *)g_object_new(G_TYPE_OBJECT, NULL); +} +#endif + +void gssdp_client_append_header(GSSDPClient *client, const char *name, const char *value) +{ + (void)client; + (void)name; + (void)value; +} + +void gssdp_client_remove_header(GSSDPClient *client, const char *name) +{ + (void)client; + (void)name; +} + +void gssdp_client_clear_headers(GSSDPClient *client) +{ + (void)client; +} + +GSSDPResourceGroup *gssdp_resource_group_new(GSSDPClient *client) +{ + (void)client; + return (GSSDPResourceGroup *)g_object_new(G_TYPE_OBJECT, NULL); +} + +guint gssdp_resource_group_add_resource_simple(GSSDPResourceGroup *resource_group, + const char *target, + const char *usn, + const char *location) +{ + (void)resource_group; + (void)target; + (void)usn; + (void)location; + return 1; +} + +void gssdp_resource_group_set_available(GSSDPResourceGroup *resource_group, gboolean available) +{ + (void)resource_group; + (void)available; +} + +void gssdp_resource_group_remove_resource(GSSDPResourceGroup *resource_group, guint resource_id) +{ + (void)resource_group; + (void)resource_id; +} diff --git a/tests/L1Tests/plat/gdial_os_stubs.cpp b/tests/L1Tests/plat/gdial_os_stubs.cpp new file mode 100644 index 00000000..38c012dc --- /dev/null +++ b/tests/L1Tests/plat/gdial_os_stubs.cpp @@ -0,0 +1,268 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2024 RDK Management + * + * 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. + */ + +/** + * @file gdial_os_stubs.cpp + * + * Stubs for the layer *below* gdial-plat-app.c: + * - gdial_os_application_* (gdial-os-app.h) + * - gdial_init / gdial_term / gdial_register_* (gdial.hpp) + * + * Also provides two gdial_plat_application_*_async symbols that are declared in + * gdial-plat-app.h but intentionally absent from gdial-plat-app.c: + * - gdial_plat_application_hide_async + * - gdial_plat_application_resume_async + * + * The gdial_plat_stub_reset_behavior / _set_app_state / _set_errors control + * interface is preserved at this layer so that any future test code that uses + * those helpers continues to work. + */ + +#include + +#include "gdial-app.h" /* GDialAppState, GDialAppError */ +#include "gdial_app_registry.h" +#include "gdial-os-app.h" +#include "gdial-plat-app.h" /* hide_async / resume_async declarations */ +#include "gdialservicecommon.h" +#include "gdial.hpp" + +/* ------------------------------------------------------------------ */ +/* Injectable error / state controls */ +/* ------------------------------------------------------------------ */ + +static GDialAppState s_app_state = GDIAL_APP_STATE_STOPPED; +static int s_start_err = 0; +static int s_hide_err = 0; +static int s_resume_err = 0; +static int s_stop_err = 0; +static int s_state_err = 0; +static gdial_registerapps_cb s_registerapps_cb = nullptr; + +static GDialAppRegistry *create_registry_from_entry(const RegisterAppEntry *entry) +{ + if (!entry || entry->Names.empty()) { + return nullptr; + } + + GList *app_prefixes = nullptr; + GList *allowed_origins = nullptr; + GHashTable *properties = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + const char *allow_stop = entry->allowStop ? "true" : "false"; + + if (!entry->prefixes.empty()) { + app_prefixes = g_list_prepend(app_prefixes, g_strdup(entry->prefixes.c_str())); + } + if (!entry->cors.empty()) { + allowed_origins = g_list_prepend(allowed_origins, g_strdup(entry->cors.c_str())); + } + g_hash_table_insert(properties, g_strdup("allowStop"), g_strdup(allow_stop)); + + return gdial_app_registry_new( + entry->Names.c_str(), + app_prefixes, + properties, + TRUE, + TRUE, + allowed_origins); +} + +static GList *build_app_registry_list(const RegisterAppEntryList *app_list) +{ + if (!app_list) { + return nullptr; + } + + GList *g_app_list = nullptr; + for (const RegisterAppEntry *entry : app_list->getValues()) { + GDialAppRegistry *registry = create_registry_from_entry(entry); + if (registry) { + g_app_list = g_list_append(g_app_list, registry); + } + } + return g_app_list; +} + +static void free_app_registry_list_nodes(GList *g_app_list) +{ + // Ownership of GDialAppRegistry entries is transferred to callback consumers + // (same semantics as production gdial.cpp). Only free the list nodes here. + g_list_free(g_app_list); +} + +extern "C" void gdial_plat_stub_reset_behavior(void) +{ + s_app_state = GDIAL_APP_STATE_STOPPED; + s_start_err = 0; + s_hide_err = 0; + s_resume_err = 0; + s_stop_err = 0; + s_state_err = 0; + s_registerapps_cb = nullptr; +} + +extern "C" void gdial_plat_stub_set_app_state(GDialAppState state) +{ + s_app_state = state; +} + +extern "C" void gdial_plat_stub_set_errors( + GDialAppError start_err, + GDialAppError hide_err, + GDialAppError resume_err, + GDialAppError stop_err, + GDialAppError state_err) +{ + s_start_err = (int)start_err; + s_hide_err = (int)hide_err; + s_resume_err = (int)resume_err; + s_stop_err = (int)stop_err; + s_state_err = (int)state_err; +} + +/* ------------------------------------------------------------------ */ +/* gdial.hpp — GLib integration layer called by gdial-plat-app.c */ +/* ------------------------------------------------------------------ */ + +bool gdial_init(GMainContext *context) { (void)context; return true; } +void gdial_term(void) {} + +void gdial_register_activation_cb(gdial_activation_cb cb) { (void)cb; } +void gdial_register_friendlyname_cb(gdial_friendlyname_cb cb) { (void)cb; } +void gdial_register_registerapps_cb(gdial_registerapps_cb cb) { s_registerapps_cb = cb; } +void gdial_register_manufacturername_cb(gdial_manufacturername_cb cb) { (void)cb; } +void gdial_register_modelname_cb(gdial_manufacturername_cb cb) { (void)cb; } + +/* ------------------------------------------------------------------ */ +/* gdial_os_application_* — one level below gdial-plat-app.c */ +/* ------------------------------------------------------------------ */ + +int gdial_os_application_start( + const char *name, const char *payload, + const char *query, const char *url, int *instance_id) +{ + (void)name; (void)payload; (void)query; (void)url; + if (s_start_err) return s_start_err; + if (instance_id) *instance_id = 1; + return 0; +} + +int gdial_os_application_hide(const char *name, int id) +{ + (void)name; (void)id; + return s_hide_err; +} + +int gdial_os_application_resume(const char *name, int id) +{ + (void)name; (void)id; + return s_resume_err; +} + +int gdial_os_application_stop(const char *name, int id) +{ + (void)name; (void)id; + return s_stop_err; +} + +int gdial_os_application_state(const char *name, int id, GDialAppState *state) +{ + (void)name; (void)id; + if (s_state_err) return s_state_err; + if (state) *state = s_app_state; + return 0; +} + +int gdial_os_application_state_changed( + const char *name, const char *id, const char *state, const char *error) +{ + (void)name; (void)id; (void)state; (void)error; + return 0; +} + +int gdial_os_application_activation_changed( + const char *activation, const char *friendly) +{ + (void)activation; (void)friendly; + return 0; +} + +int gdial_os_application_friendlyname_changed(const char *name) +{ + (void)name; + return 0; +} + +const char *gdial_os_application_get_protocol_version(void) +{ + return "2.2.1"; +} + +int gdial_os_application_register_applications(void *p) +{ + if (s_registerapps_cb && p) { + const RegisterAppEntryList *app_config_list = static_cast(p); + GList *g_app_list = build_app_registry_list(app_config_list); + s_registerapps_cb(g_app_list); + free_app_registry_list_nodes(g_app_list); + } + return 0; +} + +void gdial_os_application_update_network_standby_mode(gboolean mode) +{ + (void)mode; +} + +int gdial_os_application_update_manufacturer_name(const char *name) +{ + (void)name; + return 0; +} + +int gdial_os_application_update_model_name(const char *model) +{ + (void)model; + return 0; +} + +int gdial_os_application_service_notification(gboolean req, void *notifier) +{ + (void)req; (void)notifier; + return 0; +} + +/* ------------------------------------------------------------------ */ +/* Async stubs — declared in gdial-plat-app.h but absent from */ +/* gdial-plat-app.c */ +/* ------------------------------------------------------------------ */ + +void *gdial_plat_application_hide_async( + const gchar *name, gint id, void *user_data) +{ + (void)name; (void)id; (void)user_data; + return nullptr; +} + +void *gdial_plat_application_resume_async( + const gchar *name, gint id, void *user_data) +{ + (void)name; (void)id; (void)user_data; + return nullptr; +} diff --git a/tests/L1Tests/plat/test_gdialAppCache.cpp b/tests/L1Tests/plat/test_gdialAppCache.cpp new file mode 100644 index 00000000..799ef0da --- /dev/null +++ b/tests/L1Tests/plat/test_gdialAppCache.cpp @@ -0,0 +1,63 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 RDK Management + * + * 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 + +#include + +#include "gdialappcache.hpp" + +class GDialAppCacheTest : public ::testing::Test { +protected: + GDialAppStatusCache cache; + std::string original_netflix_id; + std::string original_youtube_id; + + void SetUp() override + { + original_netflix_id = cache.getAppCacheId("Netflix"); + original_youtube_id = cache.getAppCacheId("YouTube"); + } + + void TearDown() override + { + // Restore static cache IDs so tests remain isolated. + cache.setAppCacheId("Netflix", original_netflix_id); + cache.setAppCacheId("YouTube", original_youtube_id); + } +}; + +TEST_F(GDialAppCacheTest, SetAppCacheId_UpdatesNetflixId) +{ + cache.setAppCacheId("Netflix", "DialNetflix_L1"); + EXPECT_EQ(cache.getAppCacheId("Netflix"), "DialNetflix_L1"); +} + +TEST_F(GDialAppCacheTest, SetAppCacheId_UpdatesYoutubeId) +{ + cache.setAppCacheId("YouTube", "DialYouTube_L1"); + EXPECT_EQ(cache.getAppCacheId("YouTube"), "DialYouTube_L1"); +} + +TEST_F(GDialAppCacheTest, SetAppCacheId_UnknownAppDoesNotChangeKnownIds) +{ + cache.setAppCacheId("UnknownApp", "Unknown_L1"); + EXPECT_EQ(cache.getAppCacheId("Netflix"), original_netflix_id); + EXPECT_EQ(cache.getAppCacheId("YouTube"), original_youtube_id); +} diff --git a/tests/L1Tests/plat/test_gdialCpp.cpp b/tests/L1Tests/plat/test_gdialCpp.cpp new file mode 100644 index 00000000..f23a5620 --- /dev/null +++ b/tests/L1Tests/plat/test_gdialCpp.cpp @@ -0,0 +1,494 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 RDK Management + * + * 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 + +#include +#include +#include +#include + +extern "C" { +#include +#include "gdial-plat-dev.h" +} + +#include "gdialservicecommon.h" + +/* + * Pull real gdial.cpp into this test TU, but rename exported symbols to avoid + * collisions with gdial_os_stubs.cpp used by the rest of the L1 suite. + */ +#define gdial_register_activation_cb gdial_cpp_test_register_activation_cb +#define gdial_register_friendlyname_cb gdial_cpp_test_register_friendlyname_cb +#define gdial_register_registerapps_cb gdial_cpp_test_register_registerapps_cb +#define gdial_register_manufacturername_cb gdial_cpp_test_register_manufacturername_cb +#define gdial_register_modelname_cb gdial_cpp_test_register_modelname_cb +#define gdial_init gdial_cpp_test_init +#define gdial_term gdial_cpp_test_term +#define parse_query gdial_cpp_test_parse_query +#define gdial_os_application_start gdial_cpp_test_os_application_start +#define gdial_os_application_stop gdial_cpp_test_os_application_stop +#define gdial_os_application_hide gdial_cpp_test_os_application_hide +#define gdial_os_application_resume gdial_cpp_test_os_application_resume +#define gdial_os_application_state gdial_cpp_test_os_application_state +#define gdial_os_application_state_changed gdial_cpp_test_os_application_state_changed +#define gdial_os_application_activation_changed gdial_cpp_test_os_application_activation_changed +#define gdial_os_application_friendlyname_changed gdial_cpp_test_os_application_friendlyname_changed +#define gdial_os_application_get_protocol_version gdial_cpp_test_os_application_get_protocol_version +#define gdial_os_application_register_applications gdial_cpp_test_os_application_register_applications +#define gdial_os_application_update_network_standby_mode gdial_cpp_test_os_application_update_network_standby_mode +#define gdial_os_application_update_manufacturer_name gdial_cpp_test_os_application_update_manufacturer_name +#define gdial_os_application_update_model_name gdial_cpp_test_os_application_update_model_name +#define gdial_os_application_service_notification gdial_cpp_test_os_application_service_notification +#include "../../../server/plat/gdial.cpp" +#undef gdial_register_activation_cb +#undef gdial_register_friendlyname_cb +#undef gdial_register_registerapps_cb +#undef gdial_register_manufacturername_cb +#undef gdial_register_modelname_cb +#undef gdial_init +#undef gdial_term +#undef parse_query +#undef gdial_os_application_start +#undef gdial_os_application_stop +#undef gdial_os_application_hide +#undef gdial_os_application_resume +#undef gdial_os_application_state +#undef gdial_os_application_state_changed +#undef gdial_os_application_activation_changed +#undef gdial_os_application_friendlyname_changed +#undef gdial_os_application_get_protocol_version +#undef gdial_os_application_register_applications +#undef gdial_os_application_update_network_standby_mode +#undef gdial_os_application_update_manufacturer_name +#undef gdial_os_application_update_model_name +#undef gdial_os_application_service_notification + +namespace { + +static int g_power_cb_calls = 0; +static std::string g_last_power_state; +static int g_manufacturer_cb_calls = 0; +static int g_model_cb_calls = 0; +static int g_activation_cb_calls = 0; +static bool g_last_activation_state = false; +static std::string g_last_activation_friendlyname; +static int g_friendlyname_cb_calls = 0; +static std::string g_last_friendlyname; +static int g_registerapps_cb_calls = 0; +static gpointer g_last_registerapps_payload = nullptr; +static int g_nwstandby_cb_calls = 0; +static bool g_last_nwstandby_mode = false; + +static void test_power_cb(const char *state) +{ + ++g_power_cb_calls; + g_last_power_state = state ? state : ""; +} + +static void test_manufacturer_cb(const char *) +{ + ++g_manufacturer_cb_calls; +} + +static void test_model_cb(const char *) +{ + ++g_model_cb_calls; +} + +static void test_activation_cb(bool state, const gchar *friendlyname) +{ + ++g_activation_cb_calls; + g_last_activation_state = state; + g_last_activation_friendlyname = friendlyname ? friendlyname : ""; +} + +static void test_friendlyname_cb(const gchar *friendlyname) +{ + ++g_friendlyname_cb_calls; + g_last_friendlyname = friendlyname ? friendlyname : ""; +} + +static void test_registerapps_cb(gpointer payload) +{ + ++g_registerapps_cb_calls; + g_last_registerapps_payload = payload; +} + +static void test_nwstandby_cb(const bool mode) +{ + ++g_nwstandby_cb_calls; + g_last_nwstandby_mode = mode; +} + +class DummyNotifier : public GDialNotifier { +public: + int launch_calls = 0; + int launch_with_params_calls = 0; + int stop_calls = 0; + int hide_calls = 0; + int resume_calls = 0; + int state_calls = 0; + + void onApplicationLaunchRequest(std::string, std::string) override + { + ++launch_calls; + } + void onApplicationLaunchRequestWithLaunchParam(std::string, std::string, std::string, std::string) override + { + ++launch_with_params_calls; + } + void onApplicationStopRequest(std::string, std::string) override + { + ++stop_calls; + } + void onApplicationHideRequest(std::string, std::string) override + { + ++hide_calls; + } + void onApplicationResumeRequest(std::string, std::string) override + { + ++resume_calls; + } + void onApplicationStateRequest(std::string, std::string) override + { + ++state_calls; + } + void updatePowerState(std::string) override {} +}; + +class GDialCppTest : public ::testing::Test { +protected: + GMainContext *ctx = nullptr; + + void SetUp() override + { + ctx = g_main_context_new(); + g_power_cb_calls = 0; + g_last_power_state.clear(); + g_manufacturer_cb_calls = 0; + g_model_cb_calls = 0; + g_activation_cb_calls = 0; + g_last_activation_state = false; + g_last_activation_friendlyname.clear(); + g_friendlyname_cb_calls = 0; + g_last_friendlyname.clear(); + g_registerapps_cb_calls = 0; + g_last_registerapps_payload = nullptr; + g_nwstandby_cb_calls = 0; + g_last_nwstandby_mode = false; + + unsetenv("SYSTEM_SLEEP_REQUEST_KEY"); + unsetenv("ENABLE_NETFLIX_STOP"); + + gdail_plat_dev_register_powerstate_cb(test_power_cb); + gdail_plat_dev_register_nwstandbymode_cb(test_nwstandby_cb); + + gdial_cpp_test_register_activation_cb(nullptr); + gdial_cpp_test_register_friendlyname_cb(nullptr); + gdial_cpp_test_register_registerapps_cb(nullptr); + gdial_cpp_test_register_manufacturername_cb(nullptr); + gdial_cpp_test_register_modelname_cb(nullptr); + } + + void TearDown() override + { + gdail_plat_dev_register_powerstate_cb(nullptr); + gdail_plat_dev_register_nwstandbymode_cb(nullptr); + + gdial_cpp_test_register_activation_cb(nullptr); + gdial_cpp_test_register_friendlyname_cb(nullptr); + gdial_cpp_test_register_registerapps_cb(nullptr); + gdial_cpp_test_register_manufacturername_cb(nullptr); + gdial_cpp_test_register_modelname_cb(nullptr); + + unsetenv("SYSTEM_SLEEP_REQUEST_KEY"); + unsetenv("ENABLE_NETFLIX_STOP"); + + gdial_cpp_test_term(); + if (ctx) { + g_main_context_unref(ctx); + ctx = nullptr; + } + } +}; + +} // namespace + +TEST_F(GDialCppTest, ParseQuery_NullReturnsEmptyMap) +{ + std::map out = gdial_cpp_test_parse_query(nullptr); + EXPECT_TRUE(out.empty()); +} + +TEST_F(GDialCppTest, ParseQuery_ValidKeyValuePairs) +{ + std::map out = gdial_cpp_test_parse_query("action=sleep&key=abc"); + ASSERT_EQ(out.size(), 2u); + EXPECT_EQ(out["action"], "sleep"); + EXPECT_EQ(out["key"], "abc"); +} + +TEST_F(GDialCppTest, ParseQuery_InvalidEscapeFallsBackToRaw) +{ + std::map out = gdial_cpp_test_parse_query("k=100%"); + ASSERT_EQ(out.size(), 1u); + EXPECT_EQ(out["k"], "100%"); +} + +TEST_F(GDialCppTest, InitAndTerm_Idempotent) +{ + EXPECT_TRUE(gdial_cpp_test_init(ctx)); + EXPECT_TRUE(gdial_cpp_test_init(ctx)); + gdial_cpp_test_term(); + /* Second term after cleanup is also safe. */ + gdial_cpp_test_term(); +} + +TEST_F(GDialCppTest, OsGetProtocolVersion_WhenUninitializedReturnsDefault) +{ + const char *ver = gdial_cpp_test_os_application_get_protocol_version(); + ASSERT_NE(ver, nullptr); + EXPECT_STREQ(ver, GDIAL_PROTOCOL_VERSION_STR); +} + +TEST_F(GDialCppTest, OsServiceNotification_UninitializedReturnsInternal) +{ + DummyNotifier n; + EXPECT_EQ(gdial_cpp_test_os_application_service_notification(TRUE, &n), GDIAL_APP_ERROR_INTERNAL); +} + +TEST_F(GDialCppTest, OsServiceNotification_AfterInitReturnsNone) +{ + DummyNotifier n; + ASSERT_TRUE(gdial_cpp_test_init(ctx)); + EXPECT_EQ(gdial_cpp_test_os_application_service_notification(TRUE, &n), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(gdial_cpp_test_os_application_service_notification(FALSE, nullptr), GDIAL_APP_ERROR_NONE); +} + +TEST_F(GDialCppTest, OsUpdateManufacturerName_NullAndUninitializedReturnInternal) +{ + EXPECT_EQ(gdial_cpp_test_os_application_update_manufacturer_name(nullptr), GDIAL_CAST_ERROR_INTERNAL); + EXPECT_EQ(gdial_cpp_test_os_application_update_manufacturer_name("Acme"), GDIAL_CAST_ERROR_INTERNAL); +} + +TEST_F(GDialCppTest, OsUpdateModelName_NullAndUninitializedReturnInternal) +{ + EXPECT_EQ(gdial_cpp_test_os_application_update_model_name(nullptr), GDIAL_CAST_ERROR_INTERNAL); + EXPECT_EQ(gdial_cpp_test_os_application_update_model_name("ModelX"), GDIAL_CAST_ERROR_INTERNAL); +} + +TEST_F(GDialCppTest, OsUpdateManufacturerAndModel_AfterInitReturnNone) +{ + ASSERT_TRUE(gdial_cpp_test_init(ctx)); + gdial_cpp_test_register_manufacturername_cb(test_manufacturer_cb); + gdial_cpp_test_register_modelname_cb(test_model_cb); + + EXPECT_EQ(gdial_cpp_test_os_application_update_manufacturer_name("Acme"), GDIAL_CAST_ERROR_NONE); + EXPECT_EQ(gdial_cpp_test_os_application_update_model_name("ModelX"), GDIAL_CAST_ERROR_NONE); + EXPECT_EQ(g_manufacturer_cb_calls, 1); + EXPECT_EQ(g_model_cb_calls, 1); +} + +TEST_F(GDialCppTest, OsApplicationStart_SystemSleepTriggersPowerOff) +{ + ASSERT_TRUE(gdial_cpp_test_init(ctx)); + int instance_id = 0; + EXPECT_EQ(gdial_cpp_test_os_application_start("system", "", "action=sleep", "", &instance_id), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(g_power_cb_calls, 1); + EXPECT_EQ(g_last_power_state, "STANDBY"); +} + +TEST_F(GDialCppTest, OsApplicationStart_SystemTogglePowerTriggersToggle) +{ + ASSERT_TRUE(gdial_cpp_test_init(ctx)); + int instance_id = 0; + EXPECT_EQ(gdial_cpp_test_os_application_start("system", "", "action=togglepower", "", &instance_id), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(g_power_cb_calls, 1); + EXPECT_EQ(g_last_power_state, "TOGGLE"); +} + +TEST_F(GDialCppTest, OsApplicationState_SystemReturnsHide) +{ + GDialAppState state = GDIAL_APP_STATE_MAX; + EXPECT_EQ(gdial_cpp_test_os_application_state("system", 1, &state), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(state, GDIAL_APP_STATE_HIDE); +} + +TEST_F(GDialCppTest, OsApplicationStart_SystemSleepWithWrongKeyReturnsInternal) +{ + ASSERT_TRUE(gdial_cpp_test_init(ctx)); + setenv("SYSTEM_SLEEP_REQUEST_KEY", "expected", 1); + int instance_id = 0; + EXPECT_EQ(gdial_cpp_test_os_application_start("system", "", "action=sleep&key=wrong", "", &instance_id), + GDIAL_APP_ERROR_INTERNAL); +} + +TEST_F(GDialCppTest, OsApplicationStart_SystemToggleWithWrongKeyReturnsInternal) +{ + ASSERT_TRUE(gdial_cpp_test_init(ctx)); + setenv("SYSTEM_SLEEP_REQUEST_KEY", "expected", 1); + int instance_id = 0; + EXPECT_EQ(gdial_cpp_test_os_application_start("system", "", "action=togglepower&key=wrong", "", &instance_id), + GDIAL_APP_ERROR_INTERNAL); +} + +TEST_F(GDialCppTest, OsApplicationStart_NonSystemWithNotifierLaunchesWithParams) +{ + DummyNotifier n; + ASSERT_TRUE(gdial_cpp_test_init(ctx)); + ASSERT_EQ(gdial_cpp_test_os_application_service_notification(TRUE, &n), GDIAL_APP_ERROR_NONE); + + int instance_id = 0; + EXPECT_EQ(gdial_cpp_test_os_application_start("Netflix", "payload", "k=v", "url", &instance_id), + GDIAL_APP_ERROR_NONE); + EXPECT_EQ(n.launch_with_params_calls, 1); +} + +TEST_F(GDialCppTest, OsApplicationStateChanged_InitializedReturnsNoneAndUpdatesState) +{ + ASSERT_TRUE(gdial_cpp_test_init(ctx)); + + EXPECT_EQ(gdial_cpp_test_os_application_state_changed("App", "id", "running", "none"), GDIAL_APP_ERROR_NONE); + + GDialAppState state = GDIAL_APP_STATE_MAX; + EXPECT_EQ(gdial_cpp_test_os_application_state("App", 1, &state), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(state, GDIAL_APP_STATE_RUNNING); +} + +TEST_F(GDialCppTest, OsApplicationState_MapsHiddenAndStopped) +{ + ASSERT_TRUE(gdial_cpp_test_init(ctx)); + + EXPECT_EQ(gdial_cpp_test_os_application_state_changed("App", "id", "hidden", "none"), GDIAL_APP_ERROR_NONE); + GDialAppState state = GDIAL_APP_STATE_MAX; + EXPECT_EQ(gdial_cpp_test_os_application_state("App", 1, &state), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(state, GDIAL_APP_STATE_HIDE); + + EXPECT_EQ(gdial_cpp_test_os_application_state_changed("App", "id", "stopped", "none"), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(gdial_cpp_test_os_application_state("App", 1, &state), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(state, GDIAL_APP_STATE_STOPPED); +} + +TEST_F(GDialCppTest, OsApplicationHideResumeStop_NonSystemPaths) +{ + DummyNotifier n; + ASSERT_TRUE(gdial_cpp_test_init(ctx)); + ASSERT_EQ(gdial_cpp_test_os_application_service_notification(TRUE, &n), GDIAL_APP_ERROR_NONE); + + /* Running -> hide succeeds and notifies observer. */ + ASSERT_EQ(gdial_cpp_test_os_application_state_changed("App", "id", "running", "none"), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(gdial_cpp_test_os_application_hide("App", 7), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(n.hide_calls, 1); + + /* Running -> resume returns bad request per implementation. */ + EXPECT_EQ(gdial_cpp_test_os_application_resume("App", 7), GDIAL_APP_ERROR_BAD_REQUEST); + + /* Stop path currently always issues request (failsafe strategy). */ + EXPECT_EQ(gdial_cpp_test_os_application_stop("App", 7), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(n.stop_calls, 1); +} + +TEST_F(GDialCppTest, OsApplicationRegisterApplications_InitializedInvokesCallback) +{ + ASSERT_TRUE(gdial_cpp_test_init(ctx)); + gdial_cpp_test_register_registerapps_cb(test_registerapps_cb); + + auto *list = new RegisterAppEntryList; + auto *entry = new RegisterAppEntry; + entry->Names = "YouTube"; + entry->prefixes = "com.google"; + entry->cors = ".youtube.com"; + entry->allowStop = true; + list->pushBack(entry); + + EXPECT_EQ(gdial_cpp_test_os_application_register_applications(list), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(g_registerapps_cb_calls, 1); + EXPECT_NE(g_last_registerapps_payload, nullptr); +} + +TEST_F(GDialCppTest, OsApplicationActivationAndFriendlyName_InitializedCallbacks) +{ + ASSERT_TRUE(gdial_cpp_test_init(ctx)); + gdial_cpp_test_register_activation_cb(test_activation_cb); + gdial_cpp_test_register_friendlyname_cb(test_friendlyname_cb); + + EXPECT_EQ(gdial_cpp_test_os_application_activation_changed("true", "LivingRoom"), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(g_activation_cb_calls, 1); + EXPECT_TRUE(g_last_activation_state); + EXPECT_EQ(g_last_activation_friendlyname, "LivingRoom"); + + EXPECT_EQ(gdial_cpp_test_os_application_activation_changed("false", "Kitchen"), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(g_activation_cb_calls, 2); + EXPECT_FALSE(g_last_activation_state); + EXPECT_EQ(g_last_activation_friendlyname, "Kitchen"); + + EXPECT_EQ(gdial_cpp_test_os_application_friendlyname_changed("Bedroom"), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(g_friendlyname_cb_calls, 1); + EXPECT_EQ(g_last_friendlyname, "Bedroom"); +} + +TEST_F(GDialCppTest, OsApplicationUpdateNetworkStandbyMode_InitializedInvokesDevCb) +{ + ASSERT_TRUE(gdial_cpp_test_init(ctx)); + + gdial_cpp_test_os_application_update_network_standby_mode(TRUE); + EXPECT_EQ(g_nwstandby_cb_calls, 1); + EXPECT_TRUE(g_last_nwstandby_mode); + + gdial_cpp_test_os_application_update_network_standby_mode(FALSE); + EXPECT_EQ(g_nwstandby_cb_calls, 2); + EXPECT_FALSE(g_last_nwstandby_mode); +} + +TEST_F(GDialCppTest, OsApplicationState_NetflixEnableStopBranchExecutes) +{ + ASSERT_TRUE(gdial_cpp_test_init(ctx)); + ASSERT_EQ(gdial_cpp_test_os_application_state_changed("Netflix", "id", "running", "none"), GDIAL_APP_ERROR_NONE); + setenv("ENABLE_NETFLIX_STOP", "true", 1); + + GDialAppState state = GDIAL_APP_STATE_MAX; + EXPECT_EQ(gdial_cpp_test_os_application_state("Netflix", 1, &state), GDIAL_APP_ERROR_NONE); + /* Stubbed GetCurrentState() returns empty string -> state forced to running. */ + EXPECT_EQ(state, GDIAL_APP_STATE_RUNNING); +} + +TEST_F(GDialCppTest, OsApplicationActivationChanged_UninitializedReturnsInternal) +{ + EXPECT_EQ(gdial_cpp_test_os_application_activation_changed("true", "TV"), GDIAL_APP_ERROR_INTERNAL); +} + +TEST_F(GDialCppTest, OsApplicationFriendlyNameChanged_UninitializedReturnsInternal) +{ + EXPECT_EQ(gdial_cpp_test_os_application_friendlyname_changed("LivingRoom"), GDIAL_APP_ERROR_INTERNAL); +} + +TEST_F(GDialCppTest, OsApplicationStateChanged_UninitializedReturnsInternal) +{ + EXPECT_EQ(gdial_cpp_test_os_application_state_changed("App", "id", "running", "none"), GDIAL_APP_ERROR_INTERNAL); +} + +TEST_F(GDialCppTest, OsApplicationRegisterApplications_UninitializedReturnsInternal) +{ + auto *list = new RegisterAppEntryList; + EXPECT_EQ(gdial_cpp_test_os_application_register_applications(list), GDIAL_APP_ERROR_INTERNAL); + /* Uninitialized path does not consume the list. */ + delete list; +} diff --git a/tests/L1Tests/plat/test_gdialPlat.cpp b/tests/L1Tests/plat/test_gdialPlat.cpp new file mode 100644 index 00000000..66b99318 --- /dev/null +++ b/tests/L1Tests/plat/test_gdialPlat.cpp @@ -0,0 +1,193 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2024 RDK Management + * + * 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. + */ + +/** + * @file test_gdialPlat.cpp + * @brief Unit tests for GDialAppRegistry (server/plat/gdial_app_registry.c) + * + * Functions under test: + * gdial_app_registry_new - object construction + * gdial_app_registry_is_allowed_origin - origin allow-list logic + * gdial_app_regstry_dispose - resource cleanup + */ + +#include + +extern "C" { +#include +#include "gdial_app_registry.h" +} + +/* ================================================================== */ +/* Fixture: auto-disposes the registry created in each test */ +/* ================================================================== */ +class GDialAppRegistryTest : public ::testing::Test { +protected: + GDialAppRegistry *registry = nullptr; + + void TearDown() override { + if (registry) { + gdial_app_regstry_dispose(registry); + registry = nullptr; + } + } +}; + +/* ================================================================== */ +/* gdial_app_registry_new */ +/* ================================================================== */ + +TEST_F(GDialAppRegistryTest, New_ValidNameReturnsNonNull) { + registry = gdial_app_registry_new("Netflix", nullptr, nullptr, + TRUE, FALSE, nullptr); + ASSERT_NE(registry, nullptr); +} + +TEST_F(GDialAppRegistryTest, New_AppNameStoredCorrectly) { + registry = gdial_app_registry_new("YouTube", nullptr, nullptr, + TRUE, FALSE, nullptr); + ASSERT_NE(registry, nullptr); + EXPECT_STREQ(registry->name, "YouTube"); +} + +TEST_F(GDialAppRegistryTest, New_NullNameReturnsNull) { + GDialAppRegistry *r = gdial_app_registry_new(nullptr, nullptr, nullptr, + TRUE, FALSE, nullptr); + EXPECT_EQ(r, nullptr); +} + +TEST_F(GDialAppRegistryTest, New_NonSingletonReturnsNull) { + /* is_singleton=FALSE is rejected by g_return_val_if_fail */ + GDialAppRegistry *r = gdial_app_registry_new("App", nullptr, nullptr, + FALSE, FALSE, nullptr); + EXPECT_EQ(r, nullptr); +} + +TEST_F(GDialAppRegistryTest, New_IsSingletonFlagSet) { + registry = gdial_app_registry_new("App", nullptr, nullptr, + TRUE, FALSE, nullptr); + ASSERT_NE(registry, nullptr); + EXPECT_TRUE(registry->is_singleton); +} + +TEST_F(GDialAppRegistryTest, New_UseAdditionalDataFlagSet) { + registry = gdial_app_registry_new("App", nullptr, nullptr, + TRUE, TRUE, nullptr); + ASSERT_NE(registry, nullptr); + EXPECT_TRUE(registry->use_additional_data); +} + +TEST_F(GDialAppRegistryTest, New_PropertiesCopiedIntoRegistry) { + GHashTable *props = g_hash_table_new_full( + g_str_hash, g_str_equal, g_free, g_free); + g_hash_table_insert(props, g_strdup("version"), g_strdup("2.0")); + + registry = gdial_app_registry_new("App", nullptr, props, + TRUE, FALSE, nullptr); + g_hash_table_destroy(props); + + ASSERT_NE(registry, nullptr); + ASSERT_NE(registry->properties, nullptr); + EXPECT_STREQ( + (gchar *)g_hash_table_lookup(registry->properties, "version"), + "2.0"); +} + +TEST_F(GDialAppRegistryTest, New_NullPropertiesLeavesNullPropertyTable) { + registry = gdial_app_registry_new("App", nullptr, nullptr, + TRUE, FALSE, nullptr); + ASSERT_NE(registry, nullptr); + EXPECT_EQ(registry->properties, nullptr); +} + +/* ================================================================== */ +/* gdial_app_registry_is_allowed_origin */ +/* ================================================================== */ + +TEST_F(GDialAppRegistryTest, IsAllowedOrigin_NullRegistryReturnsFalse) { + EXPECT_FALSE(gdial_app_registry_is_allowed_origin( + nullptr, "http://example.com")); +} + +TEST_F(GDialAppRegistryTest, IsAllowedOrigin_EmptyAllowListPermitsAll) { + /* No allowed_origins → all origins are accepted */ + registry = gdial_app_registry_new("App", nullptr, nullptr, + TRUE, FALSE, nullptr); + ASSERT_NE(registry, nullptr); + EXPECT_TRUE(gdial_app_registry_is_allowed_origin( + registry, "http://any.origin.com")); +} + +TEST_F(GDialAppRegistryTest, IsAllowedOrigin_SuffixMatchPermitsOrigin) { + GList *origins = g_list_append(nullptr, g_strdup("netflix.com")); + registry = gdial_app_registry_new("Netflix", nullptr, nullptr, + TRUE, FALSE, origins); + g_list_free_full(origins, g_free); + ASSERT_NE(registry, nullptr); + EXPECT_TRUE(gdial_app_registry_is_allowed_origin( + registry, "https://www.netflix.com")); +} + +TEST_F(GDialAppRegistryTest, IsAllowedOrigin_NonMatchingOriginDenied) { + GList *origins = g_list_append(nullptr, g_strdup("netflix.com")); + registry = gdial_app_registry_new("Netflix", nullptr, nullptr, + TRUE, FALSE, origins); + g_list_free_full(origins, g_free); + ASSERT_NE(registry, nullptr); + EXPECT_FALSE(gdial_app_registry_is_allowed_origin( + registry, "https://malicious.example.com")); +} + +TEST_F(GDialAppRegistryTest, IsAllowedOrigin_NullOriginDenied) { + GList *origins = g_list_append(nullptr, g_strdup("netflix.com")); + registry = gdial_app_registry_new("Netflix", nullptr, nullptr, + TRUE, FALSE, origins); + g_list_free_full(origins, g_free); + ASSERT_NE(registry, nullptr); + /* NULL header_origin → GDIAL_STR_ENDS_WITH returns FALSE */ + EXPECT_FALSE(gdial_app_registry_is_allowed_origin(registry, nullptr)); +} + +/* ================================================================== */ +/* gdial_app_regstry_dispose */ +/* ================================================================== */ + +TEST(GDialAppRegistryDisposeTest, Dispose_NullDoesNotCrash) { + /* g_return_if_fail(app_registry != NULL) must not segfault */ + gdial_app_regstry_dispose(nullptr); +} + +TEST(GDialAppRegistryDisposeTest, Dispose_ValidRegistryDoesNotCrash) { + GDialAppRegistry *r = gdial_app_registry_new("App", nullptr, nullptr, + TRUE, FALSE, nullptr); + ASSERT_NE(r, nullptr); + gdial_app_regstry_dispose(r); /* must not crash or leak under valgrind */ +} + +TEST(GDialAppRegistryDisposeTest, Dispose_WithPropertiesDoesNotCrash) { + GHashTable *props = g_hash_table_new_full( + g_str_hash, g_str_equal, g_free, g_free); + g_hash_table_insert(props, g_strdup("k"), g_strdup("v")); + GDialAppRegistry *r = gdial_app_registry_new("App", nullptr, props, + TRUE, FALSE, nullptr); + g_hash_table_destroy(props); + ASSERT_NE(r, nullptr); + gdial_app_regstry_dispose(r); +} + diff --git a/tests/L1Tests/plat/test_gdialPlatApp.cpp b/tests/L1Tests/plat/test_gdialPlatApp.cpp new file mode 100644 index 00000000..14e7a5b8 --- /dev/null +++ b/tests/L1Tests/plat/test_gdialPlatApp.cpp @@ -0,0 +1,421 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2024 RDK Management + * + * 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. + */ + +/** + * @file test_gdialPlatApp.cpp + * @brief Unit tests for server/plat/gdial-plat-app.c + * + * Coverage strategy: + * + * GDialPlatAppNullGuardTest — standalone tests (no gdial_plat_init). + * Tests every g_return_val_if_fail guard in the public API. Each guard + * logs a GLib critical to stderr and returns the fail value; the binary + * does NOT abort. + * + * GDialPlatAppTest (fixture) — calls gdial_plat_init(ctx) in SetUp and + * gdial_plat_term() in TearDown. Covers the lifecycle and all + * synchronous delegation paths. + * + * GDialPlatAppAsyncTest (fixture) — extends GDialPlatAppTest, pumps + * g_main_context_default() to fire the 1 ms GLib timeouts used by the + * async dispatch paths. + * + * Note on timer context: g_timeout_add_full() (used inside gdial-plat-app.c) + * attaches to the GLib thread-default context. Since no thread-default is + * pushed in the test thread, that resolves to g_main_context_default(). + * The pump helpers therefore iterate g_main_context_default(), not ctx_. + */ + +#include + +#include + +extern "C" { +#include +#include "gdial-plat-app.h" +#include "gdial-app.h" /* GDIAL_APP_INSTANCE_NONE, GDialAppState, GDialAppError */ +} + +/* ================================================================== */ +/* SECTION 1: Null-guard tests — no gdial_plat_init required */ +/* g_return_val_if_fail returns the fail value and emits a GLib */ +/* critical warning; the process is NOT aborted. */ +/* ================================================================== */ + +TEST(GDialPlatAppNullGuardTest, Init_NullContext_ReturnsInternal) { + EXPECT_EQ(gdial_plat_init(nullptr), (gint)GDIAL_APP_ERROR_INTERNAL); +} + +TEST(GDialPlatAppNullGuardTest, Start_NullName_ReturnsBadRequest) { + gint id = 0; + EXPECT_EQ(gdial_plat_application_start(nullptr, nullptr, nullptr, nullptr, &id), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, Start_NullInstanceId_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_start("Netflix", nullptr, nullptr, nullptr, nullptr), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, Hide_NullName_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_hide(nullptr, 1), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, Hide_InstanceNone_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_hide("App", GDIAL_APP_INSTANCE_NONE), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, Resume_NullName_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_resume(nullptr, 1), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, Resume_InstanceNone_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_resume("App", GDIAL_APP_INSTANCE_NONE), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, Stop_NullName_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_stop(nullptr, 1), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, Stop_InstanceNone_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_stop("App", GDIAL_APP_INSTANCE_NONE), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, State_NullName_ReturnsBadRequest) { + GDialAppState s = GDIAL_APP_STATE_STOPPED; + EXPECT_EQ(gdial_plat_application_state(nullptr, 1, &s), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, State_NullStatePtr_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_state("App", 1, nullptr), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, State_InstanceNone_ReturnsBadRequest) { + GDialAppState s = GDIAL_APP_STATE_STOPPED; + EXPECT_EQ(gdial_plat_application_state("App", GDIAL_APP_INSTANCE_NONE, &s), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, StateChanged_NullName_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_state_changed(nullptr, "id", "running", "none"), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, StateChanged_NullId_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_state_changed("App", nullptr, "running", "none"), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, StateChanged_NullState_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_state_changed("App", "id", nullptr, "none"), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, StateChanged_NullError_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_state_changed("App", "id", "running", nullptr), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, ActivationChanged_NullActivation_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_activation_changed(nullptr, "TV"), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, ActivationChanged_NullFriendlyName_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_activation_changed("true", nullptr), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, FriendlyNameChanged_Null_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_friendlyname_changed(nullptr), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, RegisterApplications_Null_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_register_applications(nullptr), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, UpdateManufacturerName_Null_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_update_manufacturer_name(nullptr), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, UpdateModelName_Null_ReturnsBadRequest) { + EXPECT_EQ(gdial_plat_application_update_model_name(nullptr), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +TEST(GDialPlatAppNullGuardTest, ServiceNotification_TrueNullNotifier_ReturnsBadRequest) { + /* guard: (notifier != NULL) || (false == isNotifyRequired) */ + EXPECT_EQ(gdial_plat_application_service_notification(TRUE, nullptr), + GDIAL_APP_ERROR_BAD_REQUEST); +} + +/* Before gdial_plat_init, gdial_plat_app_async_contexts is NULL → + * g_return_val_if_fail(gdial_plat_app_async_contexts != NULL, NULL) fires. */ +TEST(GDialPlatAppNullGuardTest, StartAsync_BeforeInit_ReturnsNull) { + void *h = gdial_plat_application_start_async("Netflix", nullptr, nullptr, nullptr, nullptr); + EXPECT_EQ(h, nullptr); +} + +TEST(GDialPlatAppNullGuardTest, StopAsync_BeforeInit_ReturnsNull) { + void *h = gdial_plat_application_stop_async("Netflix", 1, nullptr); + EXPECT_EQ(h, nullptr); +} + +TEST(GDialPlatAppNullGuardTest, StateAsync_BeforeInit_ReturnsNull) { + void *h = gdial_plat_application_state_async("Netflix", 1, nullptr); + EXPECT_EQ(h, nullptr); +} + +/* Empty-name guard: g_return_val_if_fail(app_name != NULL && strlen(app_name), NULL) */ +TEST(GDialPlatAppNullGuardTest, StartAsync_EmptyName_ReturnsNull) { + /* Must call after a gdial_plat_init so async_contexts is non-NULL. + * Use the default context for a minimal init / term pair. */ + GMainContext *ctx = g_main_context_new(); + gdial_plat_init(ctx); + void *h = gdial_plat_application_start_async("", nullptr, nullptr, nullptr, nullptr); + EXPECT_EQ(h, nullptr); + gdial_plat_term(); + g_main_context_unref(ctx); +} + +/* ================================================================== */ +/* SECTION 2: Lifecycle and synchronous delegation */ +/* ================================================================== */ + +class GDialPlatAppTest : public ::testing::Test { +protected: + GMainContext *ctx_ = nullptr; + + void SetUp() override { + ctx_ = g_main_context_new(); + gdial_plat_init(ctx_); + } + + void TearDown() override { + /* Keep base fixture teardown minimal for sync tests. Draining the + * process-global default context here can execute unrelated stale + * timers from other tests and crash before this suite progresses. + * Async fixture teardown performs the explicit drain. */ + gdial_plat_term(); + g_main_context_unref(ctx_); + ctx_ = nullptr; + } +}; + +TEST_F(GDialPlatAppTest, Init_ValidContext_ReturnsNone) { + /* gdial_plat_init was called in SetUp; assert it succeeded */ + SUCCEED(); +} + +TEST_F(GDialPlatAppTest, Init_SameContextAgain_ReturnsNone) { + /* Calling with the same context passes the guard */ + EXPECT_EQ(gdial_plat_init(ctx_), (gint)GDIAL_APP_ERROR_NONE); + /* Balance the extra ref and hash-table ref acquired by the second init */ + gdial_plat_term(); +} + +TEST_F(GDialPlatAppTest, Init_DifferentContext_ReturnsInternal) { + GMainContext *other = g_main_context_new(); + EXPECT_EQ(gdial_plat_init(other), (gint)GDIAL_APP_ERROR_INTERNAL); + g_main_context_unref(other); +} + +TEST_F(GDialPlatAppTest, RegisterCallbacks_NoCrash) { + gdail_plat_register_activation_cb(nullptr); + gdail_plat_register_friendlyname_cb(nullptr); + gdail_plat_register_registerapps_cb(nullptr); + gdail_plat_register_manufacturername_cb(nullptr); + gdail_plat_register_modelname_cb(nullptr); + SUCCEED(); +} + +TEST_F(GDialPlatAppTest, Start_ValidArgs_ReturnsNoneAndSetsId) { + gint id = 0; + EXPECT_EQ(gdial_plat_application_start("Netflix", nullptr, nullptr, nullptr, &id), + GDIAL_APP_ERROR_NONE); + EXPECT_EQ(id, 1); /* OS stub sets instance_id = 1 */ +} + +TEST_F(GDialPlatAppTest, Start_WithPayloadAndQuery_ReturnsNone) { + gint id = 0; + EXPECT_EQ(gdial_plat_application_start("YouTube", "payload", "query=1", nullptr, &id), + GDIAL_APP_ERROR_NONE); +} + +TEST_F(GDialPlatAppTest, Hide_ValidArgs_ReturnsNone) { + EXPECT_EQ(gdial_plat_application_hide("Netflix", 1), GDIAL_APP_ERROR_NONE); +} + +TEST_F(GDialPlatAppTest, Resume_ValidArgs_ReturnsNone) { + EXPECT_EQ(gdial_plat_application_resume("Netflix", 1), GDIAL_APP_ERROR_NONE); +} + +TEST_F(GDialPlatAppTest, Stop_ValidArgs_ReturnsNone) { + EXPECT_EQ(gdial_plat_application_stop("Netflix", 1), GDIAL_APP_ERROR_NONE); +} + +TEST_F(GDialPlatAppTest, State_ValidArgs_ReturnsNoneAndSetsState) { + GDialAppState state = GDIAL_APP_STATE_MAX; + EXPECT_EQ(gdial_plat_application_state("Netflix", 1, &state), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(state, GDIAL_APP_STATE_STOPPED); /* OS stub default */ +} + +TEST_F(GDialPlatAppTest, StateChanged_ValidArgs_ReturnsNone) { + EXPECT_EQ(gdial_plat_application_state_changed("Netflix", "id1", "running", "none"), + GDIAL_APP_ERROR_NONE); +} + +TEST_F(GDialPlatAppTest, ActivationChanged_ValidArgs_ReturnsNone) { + EXPECT_EQ(gdial_plat_application_activation_changed("true", "LivingRoom"), + GDIAL_APP_ERROR_NONE); +} + +TEST_F(GDialPlatAppTest, FriendlyNameChanged_ValidArgs_ReturnsNone) { + EXPECT_EQ(gdial_plat_application_friendlyname_changed("LivingRoom"), + GDIAL_APP_ERROR_NONE); +} + +TEST_F(GDialPlatAppTest, GetProtocolVersion_ReturnsNonEmpty) { + const char *ver = gdial_plat_application_get_protocol_version(); + ASSERT_NE(ver, nullptr); + EXPECT_GT(strlen(ver), 0u); +} + +TEST_F(GDialPlatAppTest, RegisterApplications_ValidPtr_ReturnsNone) { + int dummy = 42; + EXPECT_EQ(gdial_plat_application_register_applications(&dummy), GDIAL_APP_ERROR_NONE); +} + +TEST_F(GDialPlatAppTest, UpdateNetworkStandbyMode_NoCrash) { + gdial_plat_application_update_network_standby_mode(TRUE); + gdial_plat_application_update_network_standby_mode(FALSE); + SUCCEED(); +} + +TEST_F(GDialPlatAppTest, UpdateManufacturerName_ValidArgs_ReturnsNone) { + EXPECT_EQ(gdial_plat_application_update_manufacturer_name("Acme"), + GDIAL_APP_ERROR_NONE); +} + +TEST_F(GDialPlatAppTest, UpdateModelName_ValidArgs_ReturnsNone) { + EXPECT_EQ(gdial_plat_application_update_model_name("BoxV2"), + GDIAL_APP_ERROR_NONE); +} + +TEST_F(GDialPlatAppTest, ServiceNotification_FalseNullNotifier_ReturnsNone) { + /* Guard: (notifier != NULL) || (false == isNotifyRequired) — FALSE+NULL passes */ + EXPECT_EQ(gdial_plat_application_service_notification(FALSE, nullptr), + GDIAL_APP_ERROR_NONE); +} + +TEST_F(GDialPlatAppTest, ServiceNotification_TrueValidNotifier_ReturnsNone) { + int dummy = 1; + EXPECT_EQ(gdial_plat_application_service_notification(TRUE, &dummy), + GDIAL_APP_ERROR_NONE); +} + +/* ================================================================== */ +/* SECTION 3: Async dispatch tests */ +/* Timers fire on g_main_context_default(); pump that context. */ +/* ================================================================== */ + +class GDialPlatAppAsyncTest : public GDialPlatAppTest { +protected: + void SetUp() override { + GDialPlatAppTest::SetUp(); + } + + void TearDown() override { + GDialPlatAppTest::TearDown(); + } +}; + +TEST_F(GDialPlatAppAsyncTest, StartAsync_Netflix_ReturnsNonNullAndCanBeCancelled) { + void *h = gdial_plat_application_start_async("Netflix", nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(h, nullptr); + gdial_plat_application_remove_async_source(h); + SUCCEED(); +} + +TEST_F(GDialPlatAppAsyncTest, StartAsync_Youtube_ReturnsNonNullAndCanBeCancelled) { + void *h = gdial_plat_application_start_async("Youtube", nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(h, nullptr); + gdial_plat_application_remove_async_source(h); + SUCCEED(); +} + +TEST_F(GDialPlatAppAsyncTest, StartAsync_UnknownApp_ReturnsNonNullAndCanBeCancelled) { + void *h = gdial_plat_application_start_async("UnknownApp", nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(h, nullptr); + gdial_plat_application_remove_async_source(h); + SUCCEED(); +} + +TEST_F(GDialPlatAppAsyncTest, StateAsync_ValidArgs_ReturnsNonNullAndCanBeCancelled) { + void *h = gdial_plat_application_state_async("Netflix", 1, nullptr); + ASSERT_NE(h, nullptr); + gdial_plat_application_remove_async_source(h); + SUCCEED(); +} + +TEST_F(GDialPlatAppAsyncTest, StateAsync_EmptyName_ReturnsNull) { + void *h = gdial_plat_application_state_async("", 1, nullptr); + EXPECT_EQ(h, nullptr); +} + +TEST_F(GDialPlatAppAsyncTest, StopAsync_ValidArgs_ReturnsNonNullAndCanBeCancelled) { + void *h = gdial_plat_application_stop_async("Netflix", 1, nullptr); + ASSERT_NE(h, nullptr); + gdial_plat_application_remove_async_source(h); + SUCCEED(); +} + +TEST_F(GDialPlatAppAsyncTest, StopAsync_EmptyName_ReturnsNull) { + void *h = gdial_plat_application_stop_async("", 1, nullptr); + EXPECT_EQ(h, nullptr); +} + +TEST_F(GDialPlatAppAsyncTest, RemoveAsyncSource_BeforeTimerFires_NoCrash) { + /* Schedule a stop_async, then explicitly cancel it before the timer fires */ + void *h = gdial_plat_application_stop_async("Netflix", 1, nullptr); + ASSERT_NE(h, nullptr); + gdial_plat_application_remove_async_source(h); + SUCCEED(); /* handle freed by remove; no need to pump context */ +} + +TEST_F(GDialPlatAppAsyncTest, SetStateCb_AllowsCallbackRegistration_NoCrash) { + gdial_plat_application_set_state_cb( + [](gint, GDialAppState, gpointer) {}, + nullptr); + void *h = gdial_plat_application_state_async("Netflix", 1, nullptr); + ASSERT_NE(h, nullptr); + gdial_plat_application_remove_async_source(h); + SUCCEED(); +} diff --git a/tests/L1Tests/plat/test_gdialPlatDev.cpp b/tests/L1Tests/plat/test_gdialPlatDev.cpp new file mode 100644 index 00000000..f6780449 --- /dev/null +++ b/tests/L1Tests/plat/test_gdialPlatDev.cpp @@ -0,0 +1,164 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 RDK Management + * + * 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. + */ + +/** + * @file test_gdialPlatDev.cpp + * @brief Unit tests for server/plat/gdial-plat-dev.c + */ + +#include + +extern "C" { +#include +#include "gdial-plat-dev.h" +} + +namespace { + +static int g_power_cb_calls = 0; +static const char *g_last_power_state = nullptr; +static int g_nw_cb_calls = 0; +static bool g_last_nw_mode = false; + +static void reset_cb_state() +{ + g_power_cb_calls = 0; + g_last_power_state = nullptr; + g_nw_cb_calls = 0; + g_last_nw_mode = false; +} + +static void power_cb(const char *state) +{ + ++g_power_cb_calls; + g_last_power_state = state; +} + +static void nwstandby_cb(const bool mode) +{ + ++g_nw_cb_calls; + g_last_nw_mode = mode; +} + +class GDialPlatDevTest : public ::testing::Test { +protected: + void SetUp() override + { + reset_cb_state(); + gdail_plat_dev_register_powerstate_cb(nullptr); + gdail_plat_dev_register_nwstandbymode_cb(nullptr); + } + + void TearDown() override + { + gdail_plat_dev_register_powerstate_cb(nullptr); + gdail_plat_dev_register_nwstandbymode_cb(nullptr); + } +}; + +} // namespace + +TEST_F(GDialPlatDevTest, SetPowerStateOn_ReturnsTrueWithoutCallback) +{ + EXPECT_TRUE(gdial_plat_dev_set_power_state_on()); + EXPECT_EQ(g_power_cb_calls, 0); +} + +TEST_F(GDialPlatDevTest, SetPowerStateOff_ReturnsTrueWithoutCallback) +{ + EXPECT_TRUE(gdial_plat_dev_set_power_state_off()); + EXPECT_EQ(g_power_cb_calls, 0); +} + +TEST_F(GDialPlatDevTest, TogglePowerState_ReturnsTrueWithoutCallback) +{ + EXPECT_TRUE(gdial_plat_dev_toggle_power_state()); + EXPECT_EQ(g_power_cb_calls, 0); +} + +TEST_F(GDialPlatDevTest, SetPowerStateOn_InvokesCallbackWithOn) +{ + gdail_plat_dev_register_powerstate_cb(power_cb); + + EXPECT_TRUE(gdial_plat_dev_set_power_state_on()); + ASSERT_EQ(g_power_cb_calls, 1); + ASSERT_NE(g_last_power_state, nullptr); + EXPECT_STREQ(g_last_power_state, "ON"); +} + +TEST_F(GDialPlatDevTest, SetPowerStateOff_InvokesCallbackWithStandby) +{ + gdail_plat_dev_register_powerstate_cb(power_cb); + + EXPECT_TRUE(gdial_plat_dev_set_power_state_off()); + ASSERT_EQ(g_power_cb_calls, 1); + ASSERT_NE(g_last_power_state, nullptr); + EXPECT_STREQ(g_last_power_state, "STANDBY"); +} + +TEST_F(GDialPlatDevTest, TogglePowerState_InvokesCallbackWithToggle) +{ + gdail_plat_dev_register_powerstate_cb(power_cb); + + EXPECT_TRUE(gdial_plat_dev_toggle_power_state()); + ASSERT_EQ(g_power_cb_calls, 1); + ASSERT_NE(g_last_power_state, nullptr); + EXPECT_STREQ(g_last_power_state, "TOGGLE"); +} + +TEST_F(GDialPlatDevTest, RegisterPowerCallback_NullClearsCallback) +{ + gdail_plat_dev_register_powerstate_cb(power_cb); + EXPECT_TRUE(gdial_plat_dev_set_power_state_on()); + EXPECT_EQ(g_power_cb_calls, 1); + + gdail_plat_dev_register_powerstate_cb(nullptr); + EXPECT_TRUE(gdial_plat_dev_set_power_state_on()); + EXPECT_EQ(g_power_cb_calls, 1); +} + +TEST_F(GDialPlatDevTest, NwStandbyModeChange_WithoutCallback_NoCrash) +{ + gdial_plat_dev_nwstandby_mode_change(true); + EXPECT_EQ(g_nw_cb_calls, 0); +} + +TEST_F(GDialPlatDevTest, NwStandbyModeChange_InvokesRegisteredCallback) +{ + gdail_plat_dev_register_nwstandbymode_cb(nwstandby_cb); + + gdial_plat_dev_nwstandby_mode_change(true); + ASSERT_EQ(g_nw_cb_calls, 1); + EXPECT_TRUE(g_last_nw_mode); + + gdial_plat_dev_nwstandby_mode_change(false); + ASSERT_EQ(g_nw_cb_calls, 2); + EXPECT_FALSE(g_last_nw_mode); +} + +TEST_F(GDialPlatDevTest, RegisterNwStandbyCallback_NullClearsCallback) +{ + gdail_plat_dev_register_nwstandbymode_cb(nwstandby_cb); + gdial_plat_dev_nwstandby_mode_change(true); + EXPECT_EQ(g_nw_cb_calls, 1); + + gdail_plat_dev_register_nwstandbymode_cb(nullptr); + gdial_plat_dev_nwstandby_mode_change(true); + EXPECT_EQ(g_nw_cb_calls, 1); +} diff --git a/tests/L1Tests/plat/test_gdialPlatUtil.cpp b/tests/L1Tests/plat/test_gdialPlatUtil.cpp new file mode 100644 index 00000000..7bf0ef2c --- /dev/null +++ b/tests/L1Tests/plat/test_gdialPlatUtil.cpp @@ -0,0 +1,115 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 RDK Management + * + * 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 +#include +#include +#include + +extern "C" { +#include "gdial-plat-util.h" +#include "gdialservicelogging.h" +} + +TEST(GDialPlatUtilTest, GetIfaceIpv4Addr_InvalidIfaceReturnsNull) { + const char *ip = gdial_plat_util_get_iface_ipv4_addr("iface_does_not_exist_123"); + EXPECT_EQ(ip, nullptr); +} + +TEST(GDialPlatUtilTest, GetIfaceIpv4Addr_LoopbackLooksValid) { + const char *ip = gdial_plat_util_get_iface_ipv4_addr("lo"); + ASSERT_NE(ip, nullptr); + EXPECT_STREQ(ip, "127.0.0.1"); +} + +TEST(GDialPlatUtilTest, GetIfaceIpv4Addr_UsesStableStaticBuffer) { + const char *ip1 = gdial_plat_util_get_iface_ipv4_addr("lo"); + ASSERT_NE(ip1, nullptr); + + const char *ip2 = gdial_plat_util_get_iface_ipv4_addr("lo"); + ASSERT_NE(ip2, nullptr); + + /* Implementation returns a pointer to a static internal buffer. */ + EXPECT_EQ(ip1, ip2); + EXPECT_STREQ(ip2, "127.0.0.1"); +} + +TEST(GDialPlatUtilTest, GetIfaceMacAddr_InvalidIfaceReturnsNull) { + const char *mac = gdial_plat_util_get_iface_mac_addr("iface_does_not_exist_123"); + EXPECT_EQ(mac, nullptr); +} + +TEST(GDialPlatUtilTest, GetIfaceMacAddr_LoopbackLooksLikeMac) { + const char *mac = gdial_plat_util_get_iface_mac_addr("lo"); + ASSERT_NE(mac, nullptr); + EXPECT_EQ(std::strlen(mac), 17u); + for (size_t i = 0; i < 17; ++i) { + if ((i + 1) % 3 == 0) { + EXPECT_EQ(mac[i], ':'); + } else { + EXPECT_TRUE(std::isxdigit(static_cast(mac[i])) != 0); + } + } +} + +TEST(GDialPlatUtilTest, GetIfaceMacAddr_UsesStableStaticBuffer) { + const char *mac1 = gdial_plat_util_get_iface_mac_addr("lo"); + ASSERT_NE(mac1, nullptr); + + const char *mac2 = gdial_plat_util_get_iface_mac_addr("lo"); + ASSERT_NE(mac2, nullptr); + + /* Implementation returns a pointer to a static internal buffer. */ + EXPECT_EQ(mac1, mac2); +} + +TEST(GDialPlatUtilLogTest, LoggerInitAndSetLevelDoNotCrash) { + gdial_plat_util_logger_init(); + gdial_plat_util_set_loglevel(INFO_LEVEL); + SUCCEED(); +} + +TEST(GDialPlatUtilLogTest, LoggerInit_UsesEnvLogLevelAndSyncStdoutDoNotCrash) { + setenv("SYNC_STDOUT", "1", 1); + setenv("GDIAL_LIBRARY_DEFAULT_LOG_LEVEL", "4", 1); + + gdial_plat_util_logger_init(); + gdial_plat_util_log(VERBOSE_LEVEL, "fn", "file.c", 3, 125, "verbose after env"); + + unsetenv("SYNC_STDOUT"); + unsetenv("GDIAL_LIBRARY_DEFAULT_LOG_LEVEL"); + SUCCEED(); +} + +TEST(GDialPlatUtilLogTest, LogAtFatalDoesNotCrash) { + gdial_plat_util_log(FATAL_LEVEL, "fn", "file.c", 1, 123, "fatal %d", 1); + SUCCEED(); +} + +TEST(GDialPlatUtilLogTest, LogAtVerboseDoesNotCrash) { + gdial_plat_util_set_loglevel(TRACE_LEVEL); + gdial_plat_util_log(VERBOSE_LEVEL, "fn", "file.c", 2, 124, "verbose %s", "ok"); + SUCCEED(); +} + +TEST(GDialPlatUtilLogTest, LogFilteredByLevelDoesNotCrash) { + gdial_plat_util_set_loglevel(INFO_LEVEL); + gdial_plat_util_log(TRACE_LEVEL, "fn", "file.c", 4, 126, "trace should be filtered"); + SUCCEED(); +} diff --git a/tests/L1Tests/server/test_gdialRest.cpp b/tests/L1Tests/server/test_gdialRest.cpp new file mode 100644 index 00000000..fb4b4d1a --- /dev/null +++ b/tests/L1Tests/server/test_gdialRest.cpp @@ -0,0 +1,486 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 RDK Management + * + * 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 +#include +#include +#include + +extern "C" { +#include +#include +#include "gdial-rest.h" +#include "gdial-rest-builder.h" +#include "gdial-app.h" + +void gdial_plat_stub_reset_behavior(void); +void gdial_plat_stub_set_app_state(GDialAppState state); +void gdial_plat_stub_set_errors( + GDialAppError start_err, + GDialAppError hide_err, + GDialAppError resume_err, + GDialAppError stop_err, + GDialAppError state_err); +} + +class GDialRestServerTest : public ::testing::Test { +protected: + const char *rest_route_id = "apps123"; + SoupServer *rest_server = nullptr; + SoupServer *local_rest_server = nullptr; + GDialRestServer *server = nullptr; + SoupSession *session = nullptr; + GMainLoop *main_loop = nullptr; + GThread *main_loop_thread = nullptr; + std::string rest_base; + std::string local_base; + + void SetUp() override { + gdial_plat_stub_reset_behavior(); + + rest_server = soup_server_new(nullptr, nullptr); + local_rest_server = soup_server_new(nullptr, nullptr); + ASSERT_NE(rest_server, nullptr); + ASSERT_NE(local_rest_server, nullptr); + + GError *error = nullptr; + ASSERT_TRUE(soup_server_listen_local(rest_server, 0, SOUP_SERVER_LISTEN_IPV4_ONLY, &error)); + ASSERT_EQ(error, nullptr); + ASSERT_TRUE(soup_server_listen_local(local_rest_server, 0, SOUP_SERVER_LISTEN_IPV4_ONLY, &error)); + ASSERT_EQ(error, nullptr); + + GSList *rest_uris = soup_server_get_uris(rest_server); + ASSERT_NE(rest_uris, nullptr); + SoupURI *rest_uri = (SoupURI *)rest_uris->data; + guint rest_port = soup_uri_get_port(rest_uri); + rest_base = std::string("http://127.0.0.1:") + std::to_string(rest_port) + "/" + rest_route_id; + g_slist_free_full(rest_uris, (GDestroyNotify)soup_uri_free); + + GSList *local_uris = soup_server_get_uris(local_rest_server); + ASSERT_NE(local_uris, nullptr); + SoupURI *local_uri = (SoupURI *)local_uris->data; + guint local_port = soup_uri_get_port(local_uri); + local_base = std::string("http://127.0.0.1:") + std::to_string(local_port); + g_slist_free_full(local_uris, (GDestroyNotify)soup_uri_free); + + main_loop = g_main_loop_new(nullptr, FALSE); + ASSERT_NE(main_loop, nullptr); + main_loop_thread = g_thread_new( + "gdial-rest-test-loop", + [](gpointer data) -> gpointer { + g_main_loop_run((GMainLoop *)data); + return nullptr; + }, + main_loop); + ASSERT_NE(main_loop_thread, nullptr); + + for (int i = 0; i < 200 && !g_main_loop_is_running(main_loop); ++i) { + g_usleep(1000); + } + ASSERT_TRUE(g_main_loop_is_running(main_loop)); + + session = soup_session_new_with_options( + SOUP_SESSION_TIMEOUT, 5, + SOUP_SESSION_IDLE_TIMEOUT, 5, + NULL); + ASSERT_NE(session, nullptr); + + server = gdial_rest_server_new(rest_server, local_rest_server, (gchar *)rest_route_id); + ASSERT_NE(server, nullptr); + g_object_set(server, "enable", TRUE, NULL); + } + + void TearDown() override { + if (server) { + gdial_rest_server_unregister_all_apps(server); + } + + if (session) { + g_object_unref(session); + session = nullptr; + } + + if (main_loop) { + g_main_loop_quit(main_loop); + } + if (main_loop_thread) { + g_thread_join(main_loop_thread); + main_loop_thread = nullptr; + } + if (main_loop) { + g_main_loop_unref(main_loop); + main_loop = nullptr; + } + + if (server) { + g_object_unref(server); + server = nullptr; + } + + if (rest_server) { + g_object_unref(rest_server); + rest_server = nullptr; + } + if (local_rest_server) { + g_object_unref(local_rest_server); + local_rest_server = nullptr; + } + + gdial_plat_stub_reset_behavior(); + + std::remove("/tmp/.dial_Netflix_uuid.txt"); + std::remove("/tmp/.dial_YouTube_uuid.txt"); + } + + static GList *make_list1(const gchar *v1) { + GList *list = nullptr; + list = g_list_prepend(list, (gpointer)v1); + return list; + } + + SoupMessage *send_rest( + const char *method, + const std::string &suffix, + const char *origin = nullptr, + const char *body = nullptr, + const char *content_type = "application/x-www-form-urlencoded") + { + std::string url = rest_base + suffix; + SoupMessage *msg = soup_message_new(method, url.c_str()); + EXPECT_NE(msg, nullptr); + if (!msg) { + return nullptr; + } + if (origin) { + soup_message_headers_replace(msg->request_headers, "Origin", origin); + } + if (body) { + soup_message_set_request(msg, content_type, SOUP_MEMORY_COPY, body, strlen(body)); + } + soup_session_send_message(session, msg); + return msg; + } + + SoupMessage *send_local( + const char *method, + const std::string &suffix, + const char *body = nullptr, + const char *content_type = "application/x-www-form-urlencoded") + { + std::string url = local_base + suffix; + SoupMessage *msg = soup_message_new(method, url.c_str()); + EXPECT_NE(msg, nullptr); + if (!msg) { + return nullptr; + } + if (body) { + soup_message_set_request(msg, content_type, SOUP_MEMORY_COPY, body, strlen(body)); + } + soup_session_send_message(session, msg); + return msg; + } +}; + +TEST(GDialRestHelperTest, NewAdditionalDataUrl_UnencodedLooksCorrect) { + gchar *url = gdial_rest_server_new_additional_data_url(56890, "Netflix", FALSE, "/abcd"); + ASSERT_NE(url, nullptr); + EXPECT_STREQ(url, "http://localhost:56890/abcd/dial_data"); + g_free(url); +} + +TEST(GDialRestHelperTest, NewAdditionalDataUrl_EncodedLooksUrlEncoded) { + gchar *url = gdial_rest_server_new_additional_data_url(1234, "Netflix", TRUE, "/abcd"); + ASSERT_NE(url, nullptr); + EXPECT_NE(strstr(url, "http%3A%2F%2Flocalhost%3A1234%2Fabcd%2Fdial_data"), nullptr); + g_free(url); +} + +TEST_F(GDialRestServerTest, RegisterFindAndUnregisterAppByName) { + GList *allowed_origins = make_list1(".example.com"); + + ASSERT_TRUE(gdial_rest_server_register_app( + server, "Netflix", nullptr, nullptr, TRUE, FALSE, allowed_origins)); + + EXPECT_TRUE(gdial_rest_server_is_app_registered(server, "Netflix")); + + GDialAppRegistry *registry = gdial_rest_server_find_app_registry(server, "Netflix"); + ASSERT_NE(registry, nullptr); + EXPECT_STREQ(registry->name, "Netflix"); + + EXPECT_TRUE(gdial_rest_server_unregister_app(server, "Netflix")); + EXPECT_FALSE(gdial_rest_server_is_app_registered(server, "Netflix")); + + g_list_free(allowed_origins); +} + +TEST_F(GDialRestServerTest, RegisterDuplicateAppFails) { + ASSERT_TRUE(gdial_rest_server_register_app( + server, "Netflix", nullptr, nullptr, TRUE, FALSE, nullptr)); + EXPECT_FALSE(gdial_rest_server_register_app( + server, "Netflix", nullptr, nullptr, TRUE, FALSE, nullptr)); +} + +TEST_F(GDialRestServerTest, RegisterApp_NonSingletonRejected) { + EXPECT_FALSE(gdial_rest_server_register_app( + server, "Netflix", nullptr, nullptr, FALSE, FALSE, nullptr)); +} + +TEST_F(GDialRestServerTest, FindRegistryByUuidMatchesRegisteredAppUri) { + ASSERT_TRUE(gdial_rest_server_register_app( + server, "Netflix", nullptr, nullptr, TRUE, FALSE, nullptr)); + + GDialAppRegistry *by_name = gdial_rest_server_find_app_registry(server, "Netflix"); + ASSERT_NE(by_name, nullptr); + ASSERT_TRUE(by_name->app_uri[0] == '/'); + + GDialAppRegistry *by_uuid = + gdial_rest_server_find_app_registry_by_uuid(server, &by_name->app_uri[1]); + ASSERT_NE(by_uuid, nullptr); + EXPECT_EQ(by_uuid, by_name); +} + +TEST_F(GDialRestServerTest, FindRegistryByUuid_UnknownUuidReturnsNull) { + ASSERT_TRUE(gdial_rest_server_register_app( + server, "Netflix", nullptr, nullptr, TRUE, FALSE, nullptr)); + + GDialAppRegistry *by_uuid = + gdial_rest_server_find_app_registry_by_uuid(server, "not-a-real-uuid"); + EXPECT_EQ(by_uuid, nullptr); +} + +TEST_F(GDialRestServerTest, FindRegistry_MatchesByPrefix) { + GList *prefixes = make_list1("YouTube"); + ASSERT_TRUE(gdial_rest_server_register_app( + server, "YouTube", prefixes, nullptr, TRUE, FALSE, nullptr)); + + GDialAppRegistry *registry = + gdial_rest_server_find_app_registry(server, "YouTubeTV"); + ASSERT_NE(registry, nullptr); + EXPECT_STREQ(registry->name, "YouTube"); + + g_list_free(prefixes); +} + +TEST_F(GDialRestServerTest, AllowedOrigin_NonYouTubeBehavior) { + GList *allowed_origins = make_list1(".example.com"); + ASSERT_TRUE(gdial_rest_server_register_app( + server, "Netflix", nullptr, nullptr, TRUE, FALSE, allowed_origins)); + + EXPECT_TRUE(gdial_rest_server_is_allowed_origin(server, nullptr, "Netflix")); + EXPECT_TRUE(gdial_rest_server_is_allowed_origin(server, "", "Netflix")); + EXPECT_TRUE(gdial_rest_server_is_allowed_origin(server, "https://www.example.com", "Netflix")); + EXPECT_FALSE(gdial_rest_server_is_allowed_origin(server, "https://evil.org", "Netflix")); + EXPECT_TRUE(gdial_rest_server_is_allowed_origin(server, "http://evil.org", "Netflix")); + + g_list_free(allowed_origins); +} + +TEST_F(GDialRestServerTest, AllowedOrigin_YouTubeRequiresSpecificOriginRules) { + GList *allowed_origins = make_list1(".youtube.com"); + ASSERT_TRUE(gdial_rest_server_register_app( + server, "YouTube", nullptr, nullptr, TRUE, FALSE, allowed_origins)); + + EXPECT_FALSE(gdial_rest_server_is_allowed_origin(server, nullptr, "YouTube")); + EXPECT_TRUE(gdial_rest_server_is_allowed_origin(server, "https://www.youtube.com", "YouTube")); + EXPECT_FALSE(gdial_rest_server_is_allowed_origin(server, "https://www.example.com", "YouTube")); + EXPECT_TRUE(gdial_rest_server_is_allowed_origin(server, "package:youtube", "YouTube")); + + g_list_free(allowed_origins); +} + +TEST_F(GDialRestServerTest, RegisterAppRegistryAndUnregisterAllApps) { + GDialAppRegistry *registry = + gdial_app_registry_new("Netflix", nullptr, nullptr, TRUE, FALSE, nullptr); + ASSERT_NE(registry, nullptr); + + ASSERT_TRUE(gdial_rest_server_register_app_registry(server, registry)); + EXPECT_TRUE(gdial_rest_server_is_app_registered(server, "Netflix")); + + GDialApp *app = gdial_app_new("Netflix"); + ASSERT_NE(app, nullptr); + EXPECT_EQ(gdial_app_start(app, nullptr, nullptr, nullptr, nullptr), GDIAL_APP_ERROR_NONE); + g_object_unref(app); + + EXPECT_TRUE(gdial_rest_server_unregister_all_apps(server)); + EXPECT_FALSE(gdial_rest_server_is_app_registered(server, "Netflix")); +} + +TEST_F(GDialRestServerTest, RegisterAppRegistry_DuplicateRejected) { + GDialAppRegistry *registry1 = + gdial_app_registry_new("Netflix", nullptr, nullptr, TRUE, FALSE, nullptr); + ASSERT_NE(registry1, nullptr); + ASSERT_TRUE(gdial_rest_server_register_app_registry(server, registry1)); + + GDialAppRegistry *registry2 = + gdial_app_registry_new("Netflix", nullptr, nullptr, TRUE, FALSE, nullptr); + ASSERT_NE(registry2, nullptr); + EXPECT_FALSE(gdial_rest_server_register_app_registry(server, registry2)); + + /* registry2 is not owned by server because registration failed. */ + gdial_app_regstry_dispose(registry2); + + EXPECT_TRUE(gdial_rest_server_unregister_app(server, "Netflix")); +} + +TEST_F(GDialRestServerTest, UnregisterUnknownAppReturnsFalse) { + EXPECT_FALSE(gdial_rest_server_unregister_app(server, "MissingApp")); +} + +TEST_F(GDialRestServerTest, EnablePropertyCanBeToggled) { + g_object_set(server, "enable", TRUE, NULL); + g_object_set(server, "enable", FALSE, NULL); + SUCCEED(); +} + +TEST_F(GDialRestServerTest, HttpOptionsOnAppPathReturnsNoContent) { + ASSERT_TRUE(gdial_rest_server_register_app( + server, "Netflix", nullptr, nullptr, TRUE, FALSE, nullptr)); + + SoupMessage *msg = send_rest("OPTIONS", "/Netflix"); + ASSERT_NE(msg, nullptr); + EXPECT_EQ(msg->status_code, SOUP_STATUS_NO_CONTENT); + EXPECT_STREQ( + soup_message_headers_get_one(msg->response_headers, "Access-Control-Allow-Methods"), + "GET, POST, OPTIONS"); + g_object_unref(msg); +} + +TEST_F(GDialRestServerTest, HttpPostOnAppPathCreatesInstance) { + ASSERT_TRUE(gdial_rest_server_register_app( + server, "Netflix", nullptr, nullptr, TRUE, FALSE, nullptr)); + gdial_plat_stub_set_app_state(GDIAL_APP_STATE_RUNNING); + + SoupMessage *msg = send_rest("POST", "/Netflix", nullptr, "k=v"); + ASSERT_NE(msg, nullptr); + EXPECT_EQ(msg->status_code, SOUP_STATUS_CREATED); + const char *location = soup_message_headers_get_one(msg->response_headers, "Location"); + ASSERT_NE(location, nullptr); + EXPECT_NE(strstr(location, "/Netflix/run"), nullptr); + g_object_unref(msg); +} + +TEST_F(GDialRestServerTest, HttpGetOnAppPathReturnsXml) { + ASSERT_TRUE(gdial_rest_server_register_app( + server, "Netflix", nullptr, nullptr, TRUE, FALSE, nullptr)); + + SoupMessage *msg = send_rest("GET", "/Netflix"); + ASSERT_NE(msg, nullptr); + EXPECT_EQ(msg->status_code, SOUP_STATUS_OK); + ASSERT_NE(msg->response_body, nullptr); + ASSERT_NE(msg->response_body->data, nullptr); + EXPECT_NE(strstr(msg->response_body->data, "instance_id = 1; + app->state = GDIAL_APP_STATE_RUNNING; + + SoupMessage *msg = send_rest("POST", "/Netflix/run/hide", "https://www.example.com"); + ASSERT_NE(msg, nullptr); + EXPECT_EQ(msg->status_code, SOUP_STATUS_OK); + EXPECT_STREQ( + soup_message_headers_get_one(msg->response_headers, "Content-Type"), + "text/plain; charset=utf-8"); + EXPECT_STREQ( + soup_message_headers_get_one(msg->response_headers, "Access-Control-Allow-Origin"), + "https://www.example.com"); + + g_object_unref(msg); + g_object_unref(app); + g_list_free(allowed_origins); +} + +TEST_F(GDialRestServerTest, HttpDeleteOnRunPathRunsHandleDelete) { + ASSERT_TRUE(gdial_rest_server_register_app( + server, "Netflix", nullptr, nullptr, TRUE, FALSE, nullptr)); + gdial_plat_stub_set_app_state(GDIAL_APP_STATE_RUNNING); + + GDialApp *app = gdial_app_new("Netflix"); + ASSERT_NE(app, nullptr); + app->instance_id = 1; + app->state = GDIAL_APP_STATE_RUNNING; + + SoupMessage *msg = send_rest("DELETE", "/Netflix/run"); + ASSERT_NE(msg, nullptr); + EXPECT_EQ(msg->status_code, SOUP_STATUS_OK); + g_object_unref(msg); +} + +TEST_F(GDialRestServerTest, LocalPostDialDataPathReturnsOk) { + ASSERT_TRUE(gdial_rest_server_register_app( + server, "Netflix", nullptr, nullptr, TRUE, FALSE, nullptr)); + GDialAppRegistry *registry = gdial_rest_server_find_app_registry(server, "Netflix"); + ASSERT_NE(registry, nullptr); + + std::string path = std::string(registry->app_uri) + "/dial_data"; + SoupMessage *msg = send_local("POST", path, "a=1&b=2"); + ASSERT_NE(msg, nullptr); + EXPECT_EQ(msg->status_code, SOUP_STATUS_OK); + g_object_unref(msg); +} + +TEST(GDialRestBuilderTest, BuildRunningResponseIncludesLinkAndOptions) { + void *builder = GET_APP_response_builder_new("Netflix"); + ASSERT_NE(builder, nullptr); + + GET_APP_response_builder_set_option(builder, "allowStop", "true"); + GET_APP_response_builder_set_state(builder, GDIAL_APP_STATE_RUNNING); + GET_APP_response_builder_set_link_href(builder, "run?q=1"); + GET_APP_response_builder_set_installable(builder, "http://example/install"); + GET_APP_response_builder_set_additionalData(builder, "k=v"); + + gsize len = 0; + gchar *xml = GET_APP_response_builder_build(builder, &len); + ASSERT_NE(xml, nullptr); + EXPECT_GT(len, 0u); + EXPECT_NE(strstr(xml, "Netflix"), nullptr); + EXPECT_NE(strstr(xml, "allowStop=\"true\""), nullptr); + EXPECT_NE(strstr(xml, "running"), nullptr); + EXPECT_NE(strstr(xml, "stopped"), nullptr); + EXPECT_EQ(strstr(xml, " +#include +#include + +extern "C" { +#include +#include +#include "gdial-app.h" +} + +/* ================================================================== */ +/* gdial_app_state_to_string */ +/* ================================================================== */ + +TEST(GDialAppStateToStringTest, Stopped) { + EXPECT_STREQ(gdial_app_state_to_string(GDIAL_APP_STATE_STOPPED), "stopped"); +} + +TEST(GDialAppStateToStringTest, Running) { + EXPECT_STREQ(gdial_app_state_to_string(GDIAL_APP_STATE_RUNNING), "running"); +} + +TEST(GDialAppStateToStringTest, Hide) { + EXPECT_STREQ(gdial_app_state_to_string(GDIAL_APP_STATE_HIDE), "hidden"); +} + +TEST(GDialAppStateToStringTest, MaxReturnsNull) { + EXPECT_EQ(gdial_app_state_to_string(GDIAL_APP_STATE_MAX), nullptr); +} + +/* ================================================================== */ +/* GDialAppState enum contracts */ +/* ================================================================== */ + +TEST(GDialAppStateEnumTest, StoppedIsZero) { + EXPECT_EQ(GDIAL_APP_STATE_STOPPED, 0); +} + +TEST(GDialAppStateEnumTest, MaxIsGreaterThanAllRunningStates) { + EXPECT_GT(GDIAL_APP_STATE_MAX, GDIAL_APP_STATE_STOPPED); + EXPECT_GT(GDIAL_APP_STATE_MAX, GDIAL_APP_STATE_HIDE); + EXPECT_GT(GDIAL_APP_STATE_MAX, GDIAL_APP_STATE_RUNNING); +} + +/* ================================================================== */ +/* GDialAppError enum contracts */ +/* ================================================================== */ + +TEST(GDialAppErrorEnumTest, ErrorNoneIsZero) { + EXPECT_EQ(GDIAL_APP_ERROR_NONE, 0); +} + +TEST(GDialAppErrorEnumTest, ImplErrorsExceedPublicErrors) { + /* GDIAL_APP_ERROR_IMPL_ (0x1000) must be above the state-derived errors */ + EXPECT_GT((int)GDIAL_APP_ERROR_IMPL_, (int)GDIAL_APP_ERROR_UNAUTH); +} + +/* ================================================================== */ +/* GDialApp object lifecycle */ +/* ================================================================== */ + +class GDialAppLifecycleTest : public ::testing::Test { +protected: + GDialApp *app = nullptr; + + void TearDown() override { + if (app) { + g_object_unref(app); + app = nullptr; + } + } +}; + +TEST_F(GDialAppLifecycleTest, New_ValidNameReturnsNonNull) { + app = gdial_app_new("Netflix"); + ASSERT_NE(app, nullptr); +} + +TEST_F(GDialAppLifecycleTest, New_AppNameStoredCorrectly) { + app = gdial_app_new("YouTube"); + ASSERT_NE(app, nullptr); + EXPECT_STREQ(app->name, "YouTube"); +} + +TEST_F(GDialAppLifecycleTest, New_InitialStateIsStopped) { + app = gdial_app_new("Netflix"); + ASSERT_NE(app, nullptr); + EXPECT_EQ(GDIAL_APP_GET_STATE(app), GDIAL_APP_STATE_STOPPED); +} + +TEST_F(GDialAppLifecycleTest, New_InitialInstanceIdIsZero) { + app = gdial_app_new("Netflix"); + ASSERT_NE(app, nullptr); + EXPECT_EQ(app->instance_id, 0); +} + +TEST_F(GDialAppLifecycleTest, State_UnstartedAppReturnsStopped) { + /* instance_id == NONE → gdial_app_state fast-path returns STOPPED */ + app = gdial_app_new("Netflix"); + ASSERT_NE(app, nullptr); + GDialAppError err = gdial_app_state(app); + EXPECT_EQ(err, GDIAL_APP_ERROR_NONE); + EXPECT_EQ(GDIAL_APP_GET_STATE(app), GDIAL_APP_STATE_STOPPED); +} + +TEST_F(GDialAppLifecycleTest, GetStateMacro_MatchesStructField) { + app = gdial_app_new("App"); + ASSERT_NE(app, nullptr); + /* The macro must read the same field the struct exposes */ + EXPECT_EQ(GDIAL_APP_GET_STATE(app), app->state); +} + +/* ================================================================== */ +/* Additional coverage for gdial-app.c helpers */ +/* ================================================================== */ + +class GDialAppExtendedTest : public ::testing::Test { +protected: + GDialApp *app = nullptr; + const gchar *kAppName = "CoverageApp"; + + void SetUp() override { + app = gdial_app_new(kAppName); + ASSERT_NE(app, nullptr); + } + + void TearDown() override { + /* Ensure persistent additional-data test file is removed. */ + gdial_app_remove_additional_dial_data_file(kAppName); + if (app) { + g_object_unref(app); + app = nullptr; + } + } +}; + +TEST_F(GDialAppExtendedTest, StartHideResumeStop_ReturnsNone) { + EXPECT_EQ(gdial_app_start(app, "payload", "query", "url", nullptr), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(gdial_app_hide(app), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(gdial_app_resume(app), GDIAL_APP_ERROR_NONE); + EXPECT_EQ(gdial_app_stop(app), GDIAL_APP_ERROR_NONE); +} + +TEST_F(GDialAppExtendedTest, Start_AssignsInstanceId) { + EXPECT_EQ(app->instance_id, 0); + EXPECT_EQ(gdial_app_start(app, nullptr, nullptr, nullptr, nullptr), GDIAL_APP_ERROR_NONE); + EXPECT_NE(app->instance_id, GDIAL_APP_INSTANCE_NONE); +} + +TEST_F(GDialAppExtendedTest, FindInstanceByNameAndId) { + EXPECT_EQ(gdial_app_start(app, nullptr, nullptr, nullptr, nullptr), GDIAL_APP_ERROR_NONE); + + GDialApp *by_name = gdial_app_find_instance_by_name(kAppName); + ASSERT_NE(by_name, nullptr); + EXPECT_EQ(by_name, app); + + GDialApp *by_id = gdial_app_find_instance_by_instance_id(app->instance_id); + ASSERT_NE(by_id, nullptr); + EXPECT_EQ(by_id, app); +} + +TEST_F(GDialAppExtendedTest, SetAndGetLaunchPayload) { + EXPECT_EQ(gdial_app_get_launch_payload(app), nullptr); + + gdial_app_set_launch_payload(app, "hello"); + ASSERT_NE(gdial_app_get_launch_payload(app), nullptr); + EXPECT_STREQ(gdial_app_get_launch_payload(app), "hello"); + + gdial_app_set_launch_payload(app, nullptr); + EXPECT_EQ(gdial_app_get_launch_payload(app), nullptr); +} + +TEST_F(GDialAppExtendedTest, SetGetAdditionalDialDataByKey) { + GHashTable *ht = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + g_hash_table_insert(ht, g_strdup("k1"), g_strdup("v1")); + g_hash_table_insert(ht, g_strdup("k2"), g_strdup("v2")); + + gdial_app_set_additional_dial_data(app, ht); + EXPECT_STREQ(gdial_app_get_additional_dial_data_by_key(app, "k1"), "v1"); + EXPECT_STREQ(gdial_app_get_additional_dial_data_by_key(app, "k2"), "v2"); + + GHashTable *dup = gdial_app_get_additional_dial_data(app); + ASSERT_NE(dup, nullptr); + EXPECT_EQ(g_hash_table_size(dup), (guint)2); + g_hash_table_unref(dup); + g_hash_table_destroy(ht); +} + +TEST_F(GDialAppExtendedTest, ClearAdditionalDialDataEmptiesTable) { + GHashTable *ht = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + g_hash_table_insert(ht, g_strdup("k"), g_strdup("v")); + gdial_app_set_additional_dial_data(app, ht); + g_hash_table_destroy(ht); + + gdial_app_clear_additional_dial_data(app); + EXPECT_EQ(gdial_app_get_additional_dial_data_by_key(app, "k"), nullptr); +} + +TEST_F(GDialAppExtendedTest, RefreshAdditionalDialDataReadsFromFile) { + const gchar *raw = "alpha one\r\nbeta two\r\n"; + ASSERT_TRUE(gdial_app_write_additional_dial_data(kAppName, raw, strlen(raw))); + + gdial_app_refresh_additional_dial_data(app); + EXPECT_STREQ(gdial_app_get_additional_dial_data_by_key(app, "alpha"), "one"); + EXPECT_STREQ(gdial_app_get_additional_dial_data_by_key(app, "beta"), "two"); +} + +TEST_F(GDialAppExtendedTest, FileHelpers_WriteReadRemoveRoundTrip) { + const gchar *payload = "x y\r\n"; + gchar *out = nullptr; + size_t out_len = 0; + + ASSERT_TRUE(gdial_app_write_additional_dial_data(kAppName, payload, strlen(payload))); + ASSERT_TRUE(gdial_app_read_additional_dial_data(kAppName, &out, &out_len)); + ASSERT_NE(out, nullptr); + EXPECT_EQ(out_len, strlen(payload)); + EXPECT_STREQ(out, payload); + g_free(out); + + EXPECT_TRUE(gdial_app_remove_additional_dial_data_file(kAppName)); +} + +TEST_F(GDialAppExtendedTest, StateResponseNew_GeneratesXml) { + int len = 0; + app->state = GDIAL_APP_STATE_RUNNING; + + gchar *xml = gdial_app_state_response_new( + app, + "2.2.1", + "2.2", + "urn:dial-multiscreen-org:schemas:dial", + &len); + + ASSERT_NE(xml, nullptr); + EXPECT_GT(len, 0); + EXPECT_NE(strstr(xml, "running"), nullptr); + xmlFree(xml); +} + +TEST_F(GDialAppExtendedTest, StateResponseNew_ClientBelow21MapsHiddenToStopped) { + int len = 0; + app->state = GDIAL_APP_STATE_HIDE; + + gchar *xml = gdial_app_state_response_new( + app, + "2.2.1", + "2.0", + "urn:dial-multiscreen-org:schemas:dial", + &len); + + ASSERT_NE(xml, nullptr); + EXPECT_NE(strstr(xml, "stopped"), nullptr); + xmlFree(xml); +} + diff --git a/tests/L1Tests/server/test_gdialService.cpp b/tests/L1Tests/server/test_gdialService.cpp new file mode 100644 index 00000000..b2304c5c --- /dev/null +++ b/tests/L1Tests/server/test_gdialService.cpp @@ -0,0 +1,476 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 RDK Management + * + * 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. + */ + +/** + * @file test_gdialService.cpp + * + * Coverage strategy: + * - GDialServiceImplTest: exercises gdialServiceImpl singleton, sendRequest, + * notifyResponse (via on* callbacks), and stop_GDialServer without a live + * server — covers lines 831-848, 1119-1222. + * - GDialServiceTest: calls gdialService::getInstance() which always exercises + * start_GDialServer up to the point it fails in CI (loopback-only environment + * causes rest_http_server and local_rest_http_server to conflict on port 56889) + * — covers lines 850-961 + 222-332. + * - Public API tests are conditional: they GTEST_SKIP when getInstance returns + * null, and provide full coverage when a real non-loopback interface exists. + */ + +#include + +#include +#include + +#include "gdialservice.h" +#include "gdialserviceimpl.h" +#include "gdialservicecommon.h" + +#include "gdial-plat-util.h" + +/* ================================================================== */ +/* Minimal notifier */ +/* ================================================================== */ + +class TestServiceNotifier : public GDialNotifier { +public: + int update_power_calls = 0; + + void onApplicationLaunchRequest(std::string, std::string) override {} + void onApplicationLaunchRequestWithLaunchParam(std::string, std::string, std::string, std::string) override {} + void onApplicationStopRequest(std::string, std::string) override {} + void onApplicationHideRequest(std::string, std::string) override {} + void onApplicationResumeRequest(std::string, std::string) override {} + void onApplicationStateRequest(std::string, std::string) override {} + void updatePowerState(std::string) override { ++update_power_calls; } +}; + +/* ================================================================== */ +/* Helper: find an interface that has a non-loopback IPv4 address so */ +/* that rest_http_server and local_rest_http_server can bind to */ +/* different addresses (127.0.0.1 vs ), avoiding the port */ +/* conflict that prevents start_GDialServer from succeeding. */ +/* ================================================================== */ +static const char *find_usable_iface() { + static const char *candidates[] = { + "eth0", "eth1", "ens3", "ens4", "ens5", "ens33", + "enp0s3", "enp3s0", "enp0s8", "em1", "wlan0", nullptr + }; + for (int i = 0; candidates[i]; ++i) { + const char *ip = gdial_plat_util_get_iface_ipv4_addr(candidates[i]); + if (ip && strcmp(ip, "127.0.0.1") != 0) { + return candidates[i]; + } + } + return nullptr; /* only loopback available; start_GDialServer will fail */ +} + +/* ================================================================== */ +/* SECTION 1: gdialServiceImpl standalone tests */ +/* These always run and cover getInstance, destroyInstance, */ +/* stop_GDialServer (null-thread path), sendRequest, notifyResponse, */ +/* and all on* virtual method bodies in gdialservice.cpp. */ +/* ================================================================== */ + +class GDialServiceImplTest : public ::testing::Test { +protected: + TestServiceNotifier notifier; + + void SetUp() override { + gdialServiceImpl::destroyInstance(); + } + void TearDown() override { + gdialServiceImpl::destroyInstance(); + } +}; + +TEST_F(GDialServiceImplTest, GetInstance_ReturnsNonNull) { + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + ASSERT_NE(impl, nullptr); +} + +TEST_F(GDialServiceImplTest, GetInstance_ReturnsSingleton) { + gdialServiceImpl *a = gdialServiceImpl::getInstance(); + gdialServiceImpl *b = gdialServiceImpl::getInstance(); + EXPECT_EQ(a, b); +} + +TEST_F(GDialServiceImplTest, DestroyInstance_NoCrash) { + /* Covers destroyInstance + stop_GDialServer when no threads are running */ + gdialServiceImpl::getInstance(); + gdialServiceImpl::destroyInstance(); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, SendRequest_AppStateChanged_NoCrash) { + /* Covers sendRequest() body (lines ~1119-1127) */ + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + ASSERT_NE(impl, nullptr); + + RequestHandlerPayload p = {}; + p.event = APP_STATE_CHANGED; + p.appNameOrfriendlyname = "Netflix"; + p.appIdOractivation = "id1"; + p.state = "running"; + p.error = "none"; + impl->sendRequest(p); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, SendRequest_ActivationChanged_NoCrash) { + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + RequestHandlerPayload p = {}; + p.event = ACTIVATION_CHANGED; + p.appIdOractivation = "true"; + p.appNameOrfriendlyname = "TV"; + impl->sendRequest(p); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, SendRequest_FriendlyNameChanged_NoCrash) { + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + RequestHandlerPayload p = {}; + p.event = FRIENDLYNAME_CHANGED; + p.appNameOrfriendlyname = "LivingRoom"; + impl->sendRequest(p); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, SendRequest_RegisterApplications_NoCrash) { + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + RequestHandlerPayload p = {}; + p.event = REGISTER_APPLICATIONS; + p.data_param = nullptr; + impl->sendRequest(p); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, SendRequest_UpdateNwStandby_NoCrash) { + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + RequestHandlerPayload p = {}; + p.event = UPDATE_NW_STANDBY; + p.user_param1 = true; + impl->sendRequest(p); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, SendRequest_UpdateManufacturer_NoCrash) { + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + RequestHandlerPayload p = {}; + p.event = UPDATE_MANUFACTURER_NAME; + p.manufacturer = "Acme"; + impl->sendRequest(p); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, SendRequest_UpdateModel_NoCrash) { + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + RequestHandlerPayload p = {}; + p.event = UPDATE_MODEL_NAME; + p.model = "BoxV2"; + impl->sendRequest(p); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, OnApplicationLaunchRequest_NoCrash) { + /* Covers onApplicationLaunchRequest + notifyResponse (lines ~1153-1137) */ + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + impl->onApplicationLaunchRequest("App1", "param1"); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, OnApplicationLaunchRequestWithParams_NoCrash) { + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + impl->onApplicationLaunchRequestWithLaunchParam("App", "payload", "query", "url"); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, OnApplicationStopRequest_NoCrash) { + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + impl->onApplicationStopRequest("App1", "id1"); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, OnApplicationHideRequest_NoCrash) { + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + impl->onApplicationHideRequest("App1", "id1"); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, OnApplicationResumeRequest_NoCrash) { + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + impl->onApplicationResumeRequest("App1", "id1"); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, OnApplicationStateRequest_NoCrash) { + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + impl->onApplicationStateRequest("App1", "id1"); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, UpdatePowerState_NullObserver_NoCrash) { + /* m_observer is null (setService never called) - covers null branch */ + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + impl->updatePowerState("STANDBY"); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, UpdatePowerState_WithObserver_CallsThrough) { + /* Covers the m_observer branch in updatePowerState (line ~1219) */ + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + impl->setService(¬ifier); + impl->updatePowerState("ON"); + EXPECT_EQ(notifier.update_power_calls, 1); +} + +/* ================================================================== */ +/* SECTION 2: gdialService lifecycle — covers getInstance failure and */ +/* conditional success paths, plus destroyInstance. */ +/* ================================================================== */ + +class GDialServiceTest : public ::testing::Test { +protected: + TestServiceNotifier notifier; + gdialService *svc = nullptr; + + void SetUp() override { + gdialService::destroyInstance(); + + std::vector args; + const char *iface = find_usable_iface(); + if (iface) { + args.push_back("--network-interface"); + args.push_back(iface); + } + + svc = gdialService::getInstance(¬ifier, args, "L1Test"); + } + + void TearDown() override { + gdialService::destroyInstance(); + svc = nullptr; + } +}; + +TEST_F(GDialServiceTest, GetInstance_NoCrash) { + /* Covers getInstance() call path including start_GDialServer attempt */ + SUCCEED(); +} + +TEST_F(GDialServiceTest, DestroyInstance_NoCrash) { + gdialService::destroyInstance(); + svc = nullptr; + SUCCEED(); +} + +TEST_F(GDialServiceTest, ApplicationStateChanged_ReturnsNone) { + if (!svc) GTEST_SKIP() << "Service start failed (loopback-only CI env)"; + EXPECT_EQ(svc->ApplicationStateChanged("Netflix", "running", "id1", "none"), + GDIAL_SERVICE_ERROR_NONE); +} + +TEST_F(GDialServiceTest, ActivationChanged_TrueAndFalse_ReturnsNone) { + if (!svc) GTEST_SKIP() << "Service start failed (loopback-only CI env)"; + EXPECT_EQ(svc->ActivationChanged("true", "TV"), GDIAL_SERVICE_ERROR_NONE); + EXPECT_EQ(svc->ActivationChanged("false", "TV"), GDIAL_SERVICE_ERROR_NONE); +} + +TEST_F(GDialServiceTest, FriendlyNameChanged_ReturnsNone) { + if (!svc) GTEST_SKIP() << "Service start failed (loopback-only CI env)"; + EXPECT_EQ(svc->FriendlyNameChanged("LivingRoom"), GDIAL_SERVICE_ERROR_NONE); +} + +TEST_F(GDialServiceTest, RegisterApplications_WithList_ReturnsNone) { + if (!svc) GTEST_SKIP() << "Service start failed (loopback-only CI env)"; + RegisterAppEntryList *list = new RegisterAppEntryList; + RegisterAppEntry *e = new RegisterAppEntry; + e->Names = "Netflix"; + e->cors = ".netflix.com"; + list->pushBack(e); + EXPECT_EQ(svc->RegisterApplications(list), GDIAL_SERVICE_ERROR_NONE); +} + +TEST_F(GDialServiceTest, RegisterApplications_NullList_ReturnsNone) { + if (!svc) GTEST_SKIP() << "Service start failed (loopback-only CI env)"; + EXPECT_EQ(svc->RegisterApplications(nullptr), GDIAL_SERVICE_ERROR_NONE); +} + +TEST_F(GDialServiceTest, SetNetworkStandbyMode_NoCrash) { + if (!svc) GTEST_SKIP() << "Service start failed (loopback-only CI env)"; + svc->setNetworkStandbyMode(true); + svc->setNetworkStandbyMode(false); + SUCCEED(); +} + +TEST_F(GDialServiceTest, SetManufacturerName_ReturnsNone) { + if (!svc) GTEST_SKIP() << "Service start failed (loopback-only CI env)"; + EXPECT_EQ(svc->setManufacturerName("Acme"), GDIAL_SERVICE_ERROR_NONE); +} + +TEST_F(GDialServiceTest, SetModelName_ReturnsNone) { + if (!svc) GTEST_SKIP() << "Service start failed (loopback-only CI env)"; + EXPECT_EQ(svc->setModelName("BoxV2"), GDIAL_SERVICE_ERROR_NONE); +} + +TEST_F(GDialServiceTest, GetProtocolVersion_ReturnsNonEmpty) { + if (!svc) GTEST_SKIP() << "Service start failed (loopback-only CI env)"; + EXPECT_FALSE(svc->getProtocolVersion().empty()); +} + +/* ================================================================== */ +/* SECTION 2b: start_GDialServer app_list else branch */ +/* Passes --app-list so the option-parsing else branch executes, */ +/* covering all the g_strstr_len checks for netflix/youtube/etc. */ +/* Only reachable when a non-loopback interface is present. */ +/* ================================================================== */ + +class GDialServiceWithAppListTest : public ::testing::Test { +protected: + TestServiceNotifier notifier; + gdialService *svc = nullptr; + + void SetUp() override { + gdialService::destroyInstance(); + + const char *iface = find_usable_iface(); + if (!iface) return; /* TearDown still safe; svc stays null */ + + std::vector args = { + "--network-interface", iface, + "--app-list", "netflix,youtube,youtubetv,youtubekids,amazoninstantvideo,spotify,pairing,system" + }; + svc = gdialService::getInstance(¬ifier, args, "L1Test"); + } + + void TearDown() override { + gdialService::destroyInstance(); + svc = nullptr; + } +}; + +TEST_F(GDialServiceWithAppListTest, StartWithAppList_ExercisesElseBranch) { + /* Exercises the `else { ... }` block in start_GDialServer that processes + * options_.app_list — covers all g_strstr_len checks at lines ~360-440. */ + if (!svc) GTEST_SKIP() << "Service start failed (loopback-only CI env)"; + SUCCEED(); +} + +/* ================================================================== */ +/* SECTION 3: RegisterAppEntryList and enum helpers */ +/* ================================================================== */ + +TEST(RegisterAppEntryListTest, PushBackAndGetValuesPreservesOrder) { + RegisterAppEntryList list; + + for (int i = 0; i < 3; ++i) { + RegisterAppEntry *e = new RegisterAppEntry; + e->Names = "App" + std::to_string(i); + list.pushBack(e); + } + + const auto &vals = list.getValues(); + ASSERT_EQ(vals.size(), 3u); + EXPECT_EQ(vals[0]->Names, "App0"); + EXPECT_EQ(vals[1]->Names, "App1"); + EXPECT_EQ(vals[2]->Names, "App2"); +} + +TEST(RegisterAppEntryListTest, DestructorFreesEntries) { + { + RegisterAppEntryList list; + RegisterAppEntry *e = new RegisterAppEntry; + e->Names = "YouTube"; + list.pushBack(e); + } + SUCCEED(); +} + +TEST(AppRequestEventsEnumTest, InvalidRequestIsLast) { + EXPECT_GT(INVALID_REQUEST, REGISTER_APPLICATIONS); +} + +TEST(AppResponseEventsEnumTest, InvalidStateIsLast) { + EXPECT_GT(APP_INVALID_STATE, APP_RESUME_REQUEST); +} + +/* ------------------------------------------------------------------ */ +/* SECTION 4: server_register_application coverage */ +/* */ +/* Note: server_register_application is a static callback registered */ +/* during service startup. It's invoked via the callback chain: */ +/* sendRequest(REGISTER_APPLICATIONS) -> */ +/* gdial_plat_application_register_applications() -> */ +/* [stub invokes callback with GList*] -> */ +/* server_register_application() */ +/* */ +/* The actual callback invocation with proper GList* construction is */ +/* tested in test_gdialCpp.cpp via GDialCastObject::registerApps(). */ +/* These tests verify the service layer handles REGISTER_APPLICATIONS */ +/* events without crashing. */ +/* ------------------------------------------------------------------ */ + +TEST_F(GDialServiceImplTest, RegisterApplications_WithNullList_NoCrash) { + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + RequestHandlerPayload p = {}; + p.event = REGISTER_APPLICATIONS; + p.data_param = nullptr; + // Verify service handles null app list without crash + impl->sendRequest(p); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, RegisterApplications_WithAppList_NoCrash) { + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + RegisterAppEntryList *list = new RegisterAppEntryList; + RegisterAppEntry *entry = new RegisterAppEntry; + entry->Names = "TestApp"; + entry->prefixes = "com.test"; + entry->cors = ".test.com"; + entry->allowStop = true; + list->pushBack(entry); + + RequestHandlerPayload p = {}; + p.event = REGISTER_APPLICATIONS; + p.data_param = list; + // Verify service handles populated app list without crash + impl->sendRequest(p); + SUCCEED(); +} + +TEST_F(GDialServiceImplTest, RegisterApplications_MultipleEntries_NoCrash) { + gdialServiceImpl *impl = gdialServiceImpl::getInstance(); + RegisterAppEntryList *list = new RegisterAppEntryList; + + // Add multiple app entries + for (int i = 0; i < 3; ++i) { + RegisterAppEntry *entry = new RegisterAppEntry; + entry->Names = "App" + std::to_string(i); + entry->prefixes = "com.test" + std::to_string(i); + entry->cors = ".test.com"; + entry->allowStop = (i % 2 == 0); + list->pushBack(entry); + } + + RequestHandlerPayload p = {}; + p.event = REGISTER_APPLICATIONS; + p.data_param = list; + // Verify service handles multiple entries without crash + impl->sendRequest(p); + SUCCEED(); +} diff --git a/tests/L1Tests/server/test_gdialShield.cpp b/tests/L1Tests/server/test_gdialShield.cpp new file mode 100644 index 00000000..6a361625 --- /dev/null +++ b/tests/L1Tests/server/test_gdialShield.cpp @@ -0,0 +1,166 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 RDK Management + * + * 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 +#include + +extern "C" { +#include +#include +#include "gdial-shield.h" +} + +namespace { + +static void ping_handler( + SoupServer *server, + SoupMessage *msg, + const char *path, + GHashTable *query, + SoupClientContext *client, + gpointer user_data) +{ + (void)server; + (void)path; + (void)query; + (void)client; + (void)user_data; + soup_message_set_status(msg, SOUP_STATUS_OK); + const char *payload = "pong"; + soup_message_set_response(msg, "text/plain", SOUP_MEMORY_COPY, payload, 4); +} + +class GDialShieldTest : public ::testing::Test { +protected: + SoupServer *server = nullptr; + SoupSession *session = nullptr; + GMainLoop *main_loop = nullptr; + GThread *main_loop_thread = nullptr; + bool shield_inited = false; + std::string base_url; + + void SetUp() override { + server = soup_server_new(nullptr, nullptr); + ASSERT_NE(server, nullptr); + + GError *error = nullptr; + ASSERT_TRUE(soup_server_listen_local(server, 0, SOUP_SERVER_LISTEN_IPV4_ONLY, &error)); + ASSERT_EQ(error, nullptr); + + GSList *uris = soup_server_get_uris(server); + ASSERT_NE(uris, nullptr); + SoupURI *uri = (SoupURI *)uris->data; + guint port = soup_uri_get_port(uri); + base_url = std::string("http://127.0.0.1:") + std::to_string(port); + g_slist_free_full(uris, (GDestroyNotify)soup_uri_free); + + soup_server_add_handler(server, "/ping", ping_handler, nullptr, nullptr); + + main_loop = g_main_loop_new(nullptr, FALSE); + ASSERT_NE(main_loop, nullptr); + main_loop_thread = g_thread_new( + "gdial-shield-test-loop", + [](gpointer data) -> gpointer { + g_main_loop_run((GMainLoop *)data); + return nullptr; + }, + main_loop); + ASSERT_NE(main_loop_thread, nullptr); + + for (int i = 0; i < 200 && !g_main_loop_is_running(main_loop); ++i) { + g_usleep(1000); + } + ASSERT_TRUE(g_main_loop_is_running(main_loop)); + + session = soup_session_new_with_options( + SOUP_SESSION_TIMEOUT, 5, + SOUP_SESSION_IDLE_TIMEOUT, 5, + NULL); + ASSERT_NE(session, nullptr); + } + + void TearDown() override { + if (shield_inited) { + gdial_shield_term(); + shield_inited = false; + } + + if (session) { + g_object_unref(session); + session = nullptr; + } + + if (main_loop) { + g_main_loop_quit(main_loop); + } + if (main_loop_thread) { + g_thread_join(main_loop_thread); + main_loop_thread = nullptr; + } + if (main_loop) { + g_main_loop_unref(main_loop); + main_loop = nullptr; + } + + if (server) { + g_object_unref(server); + server = nullptr; + } + } + + SoupMessage *send_get(const std::string &suffix) { + std::string url = base_url + suffix; + SoupMessage *msg = soup_message_new("GET", url.c_str()); + EXPECT_NE(msg, nullptr); + if (!msg) { + return nullptr; + } + soup_session_send_message(session, msg); + return msg; + } +}; + +TEST_F(GDialShieldTest, InitServerTerm_NoCrash) { + gdial_shield_init(); + shield_inited = true; + + gdial_shield_server(server); + + gdial_shield_term(); + shield_inited = false; + + SUCCEED(); +} + +TEST_F(GDialShieldTest, ShieldedServer_ProcessesBasicRequest) { + gdial_shield_init(); + shield_inited = true; + + gdial_shield_server(server); + + SoupMessage *msg = send_get("/ping"); + ASSERT_NE(msg, nullptr); + EXPECT_EQ(msg->status_code, SOUP_STATUS_OK); + ASSERT_NE(msg->response_body, nullptr); + ASSERT_NE(msg->response_body->data, nullptr); + EXPECT_NE(strstr(msg->response_body->data, "pong"), nullptr); + g_object_unref(msg); +} + +} // namespace diff --git a/tests/L1Tests/server/test_gdialSsdp.cpp b/tests/L1Tests/server/test_gdialSsdp.cpp new file mode 100644 index 00000000..01f0655b --- /dev/null +++ b/tests/L1Tests/server/test_gdialSsdp.cpp @@ -0,0 +1,247 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 RDK Management + * + * 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 + +#include + +extern "C" { +#include +#include +#include "gdial-ssdp.h" +#include "gdial-options.h" +void gdial_ssdp_networkstandbymode_handler(const bool nwstandby); +} + +namespace { + +class GDialSsdpTest : public ::testing::Test { +protected: + SoupServer *server = nullptr; + + static std::string BuildServerBaseUrl(SoupServer *srv) { + GSList *uris = soup_server_get_uris(srv); + if (!uris) return ""; + SoupURI *uri = static_cast(uris->data); + if (!uri) { + g_slist_free(uris); + return ""; + } + + char *uri_str = soup_uri_to_string(uri, FALSE); + std::string base = uri_str ? uri_str : ""; + g_free(uri_str); + soup_uri_free(uri); + g_slist_free(uris); + return base; + } + + void SetUp() override { + server = soup_server_new(nullptr, nullptr); + ASSERT_NE(server, nullptr); + } + + void TearDown() override { + if (server) { + g_object_unref(server); + server = nullptr; + } + } +}; + +TEST_F(GDialSsdpTest, New_NullServerRejected) { + GDialOptions opt = {}; + opt.iface_name = g_strdup("lo"); + EXPECT_EQ(gdial_ssdp_new(nullptr, &opt, "uuid1"), -1); + g_free(opt.iface_name); +} + +TEST_F(GDialSsdpTest, New_NullOptionsRejected) { + EXPECT_EQ(gdial_ssdp_new(server, nullptr, "uuid1"), -1); +} + +TEST_F(GDialSsdpTest, New_NullIfaceRejected) { + GDialOptions opt = {}; + opt.iface_name = nullptr; + EXPECT_EQ(gdial_ssdp_new(server, &opt, "uuid1"), -1); +} + +TEST_F(GDialSsdpTest, Setters_WithValues_NoCrash) { + EXPECT_EQ(gdial_ssdp_set_friendlyname("Living Room"), 0); + EXPECT_EQ(gdial_ssdp_set_manufacturername("Acme"), 0); + EXPECT_EQ(gdial_ssdp_set_modelname("ModelX"), 0); +} + +TEST_F(GDialSsdpTest, Setters_WithNull_NoCrash) { + EXPECT_EQ(gdial_ssdp_set_friendlyname(nullptr), 0); + EXPECT_EQ(gdial_ssdp_set_manufacturername(nullptr), 0); + EXPECT_EQ(gdial_ssdp_set_modelname(nullptr), 0); +} + +TEST_F(GDialSsdpTest, SetAvailable_NoInit_NoCrash) { + EXPECT_EQ(gdial_ssdp_set_available(true, "Kitchen"), 0); + EXPECT_EQ(gdial_ssdp_set_available(false, "Kitchen"), 0); +} + +TEST_F(GDialSsdpTest, NetworkStandbyHandler_NoInit_NoCrash) { + gdial_ssdp_networkstandbymode_handler(true); + gdial_ssdp_networkstandbymode_handler(false); + SUCCEED(); +} + +TEST_F(GDialSsdpTest, SsdpHttpCallback_GetDdXmlReturnsOkAndHeaders) { + GError *error = nullptr; + ASSERT_TRUE(soup_server_listen_local(server, 0, SOUP_SERVER_LISTEN_IPV4_ONLY, &error)); + ASSERT_EQ(error, nullptr); + + GDialOptions opt = {}; + opt.iface_name = g_strdup("lo"); + opt.friendly_name = g_strdup("L1Friendly"); + opt.manufacturer = g_strdup("L1Maker"); + opt.model_name = g_strdup("L1Model"); + opt.uuid = g_strdup("12345678-abcd-abcd-1234-123456789abc"); + + const char *uuid = "uuid_ut"; + ASSERT_EQ(gdial_ssdp_new(server, &opt, uuid), 0); + + /* Override the process-global app_manufacturer_name / app_model_name that + * may have been set by earlier Setters tests. The HTTP callback checks + * these statics first and only falls back to gdial_options_ when NULL. */ + gdial_ssdp_set_manufacturername("L1Maker"); + gdial_ssdp_set_modelname("L1Model"); + + std::string base = BuildServerBaseUrl(server); + ASSERT_FALSE(base.empty()); + std::string url = base + uuid + "/dd.xml"; + + /* + * Use async queue_message + g_main_loop_run so that both the outbound + * client I/O and the SoupServer's inbound dispatch are handled by the + * same g_main_context_default() iteration. Blocking send_message + * deadlocks because the server needs the context iterated while the + * test thread is blocked on the socket waiting for a response. + */ + struct Ctx { + GMainLoop *loop; + guint status = 0; + std::string app_url; + std::string body; + } ctx; + ctx.loop = g_main_loop_new(nullptr, FALSE); + + /* Safety watchdog — quits the loop if no response arrives in 5 s. */ + GSource *watchdog = g_timeout_source_new_seconds(5); + g_source_set_callback(watchdog, + [](gpointer d) -> gboolean { + g_main_loop_quit(static_cast(d)); + return G_SOURCE_REMOVE; + }, ctx.loop, nullptr); + g_source_attach(watchdog, nullptr); + g_source_unref(watchdog); + + SoupSession *session = soup_session_new_with_options( + SOUP_SESSION_TIMEOUT, (guint)5, nullptr); + ASSERT_NE(session, nullptr); + SoupMessage *msg = soup_message_new("GET", url.c_str()); + ASSERT_NE(msg, nullptr); + + /* queue_message transfers ownership of msg to the session. */ + soup_session_queue_message(session, msg, + [](SoupSession *, SoupMessage *m, gpointer d) { + auto *c = static_cast(d); + c->status = m->status_code; + const char *au = soup_message_headers_get_one( + m->response_headers, "Application-URL"); + c->app_url = au ? au : ""; + if (m->response_body && m->response_body->data) + c->body.assign(m->response_body->data, + (std::string::size_type)m->response_body->length); + g_main_loop_quit(c->loop); + }, &ctx); + + g_main_loop_run(ctx.loop); + g_main_loop_unref(ctx.loop); + g_object_unref(session); + + EXPECT_EQ(ctx.status, (guint)SOUP_STATUS_OK); + EXPECT_FALSE(ctx.app_url.empty()); + EXPECT_NE(ctx.body.find("L1Friendly"), std::string::npos); + EXPECT_NE(ctx.body.find("L1Maker"), std::string::npos); + EXPECT_NE(ctx.body.find("L1Model"), std::string::npos); + + gdial_ssdp_destroy(); +} + +TEST_F(GDialSsdpTest, SsdpHttpCallback_NonGetReturnsBadRequest) { + GError *error = nullptr; + ASSERT_TRUE(soup_server_listen_local(server, 0, SOUP_SERVER_LISTEN_IPV4_ONLY, &error)); + ASSERT_EQ(error, nullptr); + + GDialOptions opt = {}; + opt.iface_name = g_strdup("lo"); + opt.friendly_name = g_strdup("L1Friendly"); + opt.manufacturer = g_strdup("L1Maker"); + opt.model_name = g_strdup("L1Model"); + opt.uuid = g_strdup("12345678-abcd-abcd-1234-123456789abc"); + + const char *uuid = "uuid_ut"; + ASSERT_EQ(gdial_ssdp_new(server, &opt, uuid), 0); + + std::string base = BuildServerBaseUrl(server); + ASSERT_FALSE(base.empty()); + std::string url = base + uuid + "/dd.xml"; + + struct Ctx { + GMainLoop *loop; + guint status = 0; + } ctx; + ctx.loop = g_main_loop_new(nullptr, FALSE); + + GSource *watchdog = g_timeout_source_new_seconds(5); + g_source_set_callback(watchdog, + [](gpointer d) -> gboolean { + g_main_loop_quit(static_cast(d)); + return G_SOURCE_REMOVE; + }, ctx.loop, nullptr); + g_source_attach(watchdog, nullptr); + g_source_unref(watchdog); + + SoupSession *session = soup_session_new_with_options( + SOUP_SESSION_TIMEOUT, (guint)5, nullptr); + ASSERT_NE(session, nullptr); + SoupMessage *msg = soup_message_new("POST", url.c_str()); + ASSERT_NE(msg, nullptr); + + soup_session_queue_message(session, msg, + [](SoupSession *, SoupMessage *m, gpointer d) { + auto *c = static_cast(d); + c->status = m->status_code; + g_main_loop_quit(c->loop); + }, &ctx); + + g_main_loop_run(ctx.loop); + g_main_loop_unref(ctx.loop); + g_object_unref(session); + + EXPECT_EQ(ctx.status, (guint)SOUP_STATUS_BAD_REQUEST); + + gdial_ssdp_destroy(); +} + +} // namespace diff --git a/tests/L1Tests/server/test_gdialserver_ut.cpp b/tests/L1Tests/server/test_gdialserver_ut.cpp new file mode 100644 index 00000000..c7c9ffb2 --- /dev/null +++ b/tests/L1Tests/server/test_gdialserver_ut.cpp @@ -0,0 +1,160 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 RDK Management + * + * 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 + +#include +#include +#include + +namespace { +int g_get_instance_calls = 0; +int g_destroy_instance_calls = 0; +int g_activation_calls = 0; +int g_register_calls = 0; + +void reset_stub_state() { + g_get_instance_calls = 0; + g_destroy_instance_calls = 0; + g_activation_calls = 0; + g_register_calls = 0; +} +} // namespace + +#define gdialService gdialServiceFake +#define main gdialserver_ut_main +#include "../../../server/gdialserver_ut.cpp" +#undef main +#undef gdialService + +gdialServiceFake* gdialServiceFake::getInstance( + GDialNotifier* observer, + const std::vector& gdial_args, + const std::string& actualprocessName) +{ + (void)observer; + (void)gdial_args; + (void)actualprocessName; + ++g_get_instance_calls; + return reinterpret_cast(0x1); +} + +void gdialServiceFake::destroyInstance() { + ++g_destroy_instance_calls; +} + +GDIAL_SERVICE_ERROR_CODES gdialServiceFake::ApplicationStateChanged( + std::string applicationName, + std::string appState, + std::string applicationId, + std::string error) +{ + (void)applicationName; + (void)appState; + (void)applicationId; + (void)error; + return GDIAL_SERVICE_ERROR_NONE; +} + +GDIAL_SERVICE_ERROR_CODES gdialServiceFake::ActivationChanged(std::string activation, std::string friendlyname) { + (void)activation; + (void)friendlyname; + ++g_activation_calls; + return GDIAL_SERVICE_ERROR_NONE; +} + +GDIAL_SERVICE_ERROR_CODES gdialServiceFake::FriendlyNameChanged(std::string friendlyname) { + (void)friendlyname; + return GDIAL_SERVICE_ERROR_NONE; +} + +std::string gdialServiceFake::getProtocolVersion(void) { + return "2.2"; +} + +GDIAL_SERVICE_ERROR_CODES gdialServiceFake::RegisterApplications(RegisterAppEntryList* appConfigList) { + ++g_register_calls; + delete appConfigList; + return GDIAL_SERVICE_ERROR_NONE; +} + +void gdialServiceFake::setNetworkStandbyMode(bool nwStandbymode) { + (void)nwStandbymode; +} + +GDIAL_SERVICE_ERROR_CODES gdialServiceFake::setManufacturerName(std::string manufacturer) { + (void)manufacturer; + return GDIAL_SERVICE_ERROR_NONE; +} + +GDIAL_SERVICE_ERROR_CODES gdialServiceFake::setModelName(std::string model) { + (void)model; + return GDIAL_SERVICE_ERROR_NONE; +} + +class GDialServerUTMainTest : public ::testing::Test { +protected: + void SetUp() override { + running = true; + } + + void TearDown() override { + running = true; + } + + int run_main_with_input(const std::string& input, int argc = 1, char** argv = nullptr) { + std::istringstream input_stream(input); + std::streambuf* old_buf = std::cin.rdbuf(input_stream.rdbuf()); + + char default_arg0[] = "gdialserver_ut"; + char* default_argv[] = { default_arg0, nullptr }; + char** use_argv = argv ? argv : default_argv; + + int ret = gdialserver_ut_main(argc, use_argv); + + std::cin.rdbuf(old_buf); + return ret; + } +}; + +TEST_F(GDialServerUTMainTest, SignalHandlerStopsRunningLoop) { + running = true; + signalHandler(SIGINT); + EXPECT_FALSE(running.load()); +} + +TEST_F(GDialServerUTMainTest, MainQuitCommandExitsAndCleansUp) { + int ret = run_main_with_input("q\n"); + + EXPECT_EQ(ret, 0); + EXPECT_EQ(g_get_instance_calls, 1); + EXPECT_EQ(g_destroy_instance_calls, 1); + EXPECT_EQ(g_activation_calls, 0); + EXPECT_EQ(g_register_calls, 0); +} + +TEST_F(GDialServerUTMainTest, MainEnableDisableRegisterRestartFlow) { + int ret = run_main_with_input("enable\ndisable\nregister\nrestart\nq\n"); + + EXPECT_EQ(ret, 0); + EXPECT_EQ(g_get_instance_calls, 3); + EXPECT_EQ(g_destroy_instance_calls, 3); + EXPECT_EQ(g_activation_calls, 2); + EXPECT_EQ(g_register_calls, 1); +} diff --git a/tests/L1Tests/stubs/gdial_cpp_test_stubs.hpp b/tests/L1Tests/stubs/gdial_cpp_test_stubs.hpp new file mode 100644 index 00000000..f35c833d --- /dev/null +++ b/tests/L1Tests/stubs/gdial_cpp_test_stubs.hpp @@ -0,0 +1,130 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 RDK Management + * + * 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. + */ + +#ifndef GDIAL_CPP_TEST_STUBS_HPP_ +#define GDIAL_CPP_TEST_STUBS_HPP_ + +#include +#include +#include + +#ifndef _T +#define _T(x) x +#endif + +namespace WPEFramework { + +class JsonObject { +public: + std::map values; + + std::string &operator[](const std::string &key) + { + return values[key]; + } +}; + +namespace Core { + +static const uint32_t ERROR_NONE = 0; + +class SystemInfo { +public: + static void SetEnvironment(const std::string &, const std::string &) {} +}; + +namespace JSON { + +class IElement {}; + +class String { +public: + String() = default; + explicit String(const std::string &v) : value_(v) {} + + const std::string &Data() const { return value_; } + + String &operator=(const std::string &v) + { + value_ = v; + return *this; + } + +private: + std::string value_; +}; + +template +class ArrayType { +public: + class Iterator { + public: + explicit Iterator(const ArrayType &) {} + bool Next() { return false; } + const T &Current() const + { + static T t; + return t; + } + }; + + const ArrayType &Elements() const { return *this; } +}; + +} // namespace JSON +} // namespace Core + +namespace PluginHost { +namespace MetaData { + +struct Service { + Core::JSON::String JSONState; +}; + +} // namespace MetaData +} // namespace PluginHost + +namespace JSONRPC { + +template +class LinkType { +public: + LinkType(const std::string &, bool, const std::string &) {} + + template + uint32_t Get(uint32_t, const std::string &, R &) + { + return Core::ERROR_NONE; + } + + uint32_t Invoke(const std::string &, const JsonObject &, JsonObject &) + { + return Core::ERROR_NONE; + } +}; + +} // namespace JSONRPC +} // namespace WPEFramework + +inline int GetSecurityToken(int, unsigned char *) +{ + return 0; +} + +#endif diff --git a/tests/L1Tests/test_main.cpp b/tests/L1Tests/test_main.cpp new file mode 100644 index 00000000..5ee18834 --- /dev/null +++ b/tests/L1Tests/test_main.cpp @@ -0,0 +1,39 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2024 RDK Management + * + * 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. +*/ + +/** + * @file test_main.cpp + * @brief Main entry point for the xdialserver L1 unit test runner + * + * Initializes Google Test and Google Mock, then runs all registered test suites. + * Individual component tests live under subdirectories (e.g. server/, plat/, utils/) + * and are compiled into this single binary. + */ + +#include +#include + +/** + * Main test entry point + * Initializes the test framework and runs all registered tests + */ +int main(int argc, char **argv) { + ::testing::InitGoogleMock(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/L1Tests/tests/gdialserver_ut.cpp b/tests/L1Tests/tests/gdialserver_ut.cpp new file mode 100644 index 00000000..3a0d1baa --- /dev/null +++ b/tests/L1Tests/tests/gdialserver_ut.cpp @@ -0,0 +1,53 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2024 RDK Management + * + * 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 +#include + +/** + * @brief Example test class for xdialserver components + * + * Add your unit tests here. This is a template structure + * that can be expanded with actual test cases. + */ +class GDialServerTest : public ::testing::Test { +protected: + /** + * @brief SetUp is called before each test + */ + void SetUp() override { + // Initialize any test fixtures here + } + + /** + * @brief TearDown is called after each test + */ + void TearDown() override { + // Clean up any test fixtures here + } +}; + +/** + * @brief Example placeholder test + * + * Replace this with actual unit tests for xdialserver components + */ +TEST_F(GDialServerTest, ExampleTest) { + EXPECT_TRUE(true); +} diff --git a/tests/L1Tests/utils/test_gdialUtil.cpp b/tests/L1Tests/utils/test_gdialUtil.cpp new file mode 100644 index 00000000..fb94c2f5 --- /dev/null +++ b/tests/L1Tests/utils/test_gdialUtil.cpp @@ -0,0 +1,314 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2024 RDK Management + * + * 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. + */ + +/** + * @file test_gdialUtil.cpp + * @brief Unit tests for gdial-util.c (server/gdial-util.h) + * + * Functions under test: + * gdial_util_is_ascii_printable + * gdial_util_str_str_hashtable_to_string + * gdial_util_str_str_hashtable_from_string + * gdial_util_str_str_hashtable_to_xml_string + * gdial_util_str_str_hashtable_dup + * gdial_util_str_str_hashtable_equal + * gdial_util_str_str_hashtable_merge + */ + +#include +#include + +extern "C" { +#include +#include "gdial-util.h" +} + +/* ================================================================== */ +/* Fixture: owns two GHashTable* and destroys them in TearDown */ +/* ================================================================== */ +class GDialUtilHashTableTest : public ::testing::Test { +protected: + GHashTable *ht1 = nullptr; + GHashTable *ht2 = nullptr; + + void SetUp() override { + ht1 = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + ht2 = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + } + + void TearDown() override { + if (ht1) { g_hash_table_destroy(ht1); ht1 = nullptr; } + if (ht2) { g_hash_table_destroy(ht2); ht2 = nullptr; } + } +}; + +/* ================================================================== */ +/* gdial_util_is_ascii_printable */ +/* ================================================================== */ + +TEST(GDialUtilTest, IsPrintable_AllPrintableChars) { + const gchar *s = "Hello World!"; + EXPECT_TRUE(gdial_util_is_ascii_printable(s, strlen(s))); +} + +TEST(GDialUtilTest, IsPrintable_WithWhitespace) { + /* Tabs and newlines count as g_ascii_isspace → should pass */ + const gchar *s = "foo\tbar\n"; + EXPECT_TRUE(gdial_util_is_ascii_printable(s, strlen(s))); +} + +TEST(GDialUtilTest, IsPrintable_NonPrintableControlChar) { + const gchar data[] = { '\x01', '\x02', '\0' }; + EXPECT_FALSE(gdial_util_is_ascii_printable(data, 2)); +} + +TEST(GDialUtilTest, IsPrintable_NullDataReturnsFalse) { + EXPECT_FALSE(gdial_util_is_ascii_printable(nullptr, 4)); +} + +TEST(GDialUtilTest, IsPrintable_ZeroLengthReturnsFalse) { + EXPECT_FALSE(gdial_util_is_ascii_printable("abc", 0)); +} + +/* ================================================================== */ +/* gdial_util_str_str_hashtable_to_string */ +/* ================================================================== */ + +TEST_F(GDialUtilHashTableTest, ToStr_ContainsKeyAndValue) { + g_hash_table_insert(ht1, g_strdup("k"), g_strdup("v")); + gsize len = 0; + gchar *s = gdial_util_str_str_hashtable_to_string(ht1, "=", FALSE, &len); + ASSERT_NE(s, nullptr); + EXPECT_NE(g_strstr_len(s, -1, "k"), nullptr); + EXPECT_NE(g_strstr_len(s, -1, "v"), nullptr); + EXPECT_GT(len, (gsize)0); + g_free(s); +} + +TEST_F(GDialUtilHashTableTest, ToStr_NullDelimiterDefaultsToSpace) { + g_hash_table_insert(ht1, g_strdup("k"), g_strdup("v")); + gsize len = 0; + gchar *s = gdial_util_str_str_hashtable_to_string(ht1, nullptr, FALSE, &len); + ASSERT_NE(s, nullptr); + EXPECT_NE(g_strstr_len(s, -1, " "), nullptr); + g_free(s); +} + +TEST_F(GDialUtilHashTableTest, ToStr_WithNewline) { + g_hash_table_insert(ht1, g_strdup("key"), g_strdup("val")); + gsize len = 0; + gchar *s = gdial_util_str_str_hashtable_to_string(ht1, "=", TRUE, &len); + ASSERT_NE(s, nullptr); + EXPECT_NE(g_strstr_len(s, -1, "\r\n"), nullptr); + g_free(s); +} + +TEST_F(GDialUtilHashTableTest, ToStr_NullTableReturnsNull) { + gsize len = 0; + EXPECT_EQ(gdial_util_str_str_hashtable_to_string(nullptr, "=", FALSE, &len), nullptr); +} + +TEST_F(GDialUtilHashTableTest, ToStr_NullLengthReturnsNull) { + g_hash_table_insert(ht1, g_strdup("k"), g_strdup("v")); + EXPECT_EQ(gdial_util_str_str_hashtable_to_string(ht1, "=", FALSE, nullptr), nullptr); +} + +/* ================================================================== */ +/* gdial_util_str_str_hashtable_from_string */ +/* ================================================================== */ + +TEST_F(GDialUtilHashTableTest, FromStr_ParsesSinglePair) { + /* Format expected by the parser: "key val\r\n" */ + const gchar *str = "mykey myval\r\n"; + EXPECT_TRUE(gdial_util_str_str_hashtable_from_string(str, strlen(str), ht1)); + EXPECT_STREQ((gchar *)g_hash_table_lookup(ht1, "mykey"), "myval"); +} + +TEST_F(GDialUtilHashTableTest, FromStr_NullStringReturnsFalse) { + EXPECT_FALSE(gdial_util_str_str_hashtable_from_string(nullptr, 4, ht1)); +} + +TEST_F(GDialUtilHashTableTest, FromStr_NullTableReturnsFalse) { + EXPECT_FALSE(gdial_util_str_str_hashtable_from_string("k v\r\n", 6, nullptr)); +} + +TEST_F(GDialUtilHashTableTest, FromStr_ParsesMultiplePairs) { + const gchar *str = "k1 v1\r\nk2 v2\r\n"; + EXPECT_TRUE(gdial_util_str_str_hashtable_from_string(str, strlen(str), ht1)); + EXPECT_STREQ((gchar *)g_hash_table_lookup(ht1, "k1"), "v1"); + EXPECT_STREQ((gchar *)g_hash_table_lookup(ht1, "k2"), "v2"); +} + +TEST_F(GDialUtilHashTableTest, FromStr_ZeroLengthNoEntries) { + EXPECT_TRUE(gdial_util_str_str_hashtable_from_string("k v\r\n", 0, ht1)); + EXPECT_EQ(g_hash_table_size(ht1), (guint)0); +} + +/* ================================================================== */ +/* gdial_util_str_str_hashtable_to_xml_string */ +/* ================================================================== */ + +TEST_F(GDialUtilHashTableTest, ToXml_ContainsAttrEqualsValue) { + g_hash_table_insert(ht1, g_strdup("xmlns"), g_strdup("urn:test")); + gsize len = 0; + gchar *s = gdial_util_str_str_hashtable_to_xml_string(ht1, &len); + ASSERT_NE(s, nullptr); + EXPECT_NE(g_strstr_len(s, -1, "xmlns"), nullptr); + EXPECT_NE(g_strstr_len(s, -1, "urn:test"), nullptr); + g_free(s); +} + +TEST_F(GDialUtilHashTableTest, ToXml_NullTableReturnsNull) { + gsize len = 0; + EXPECT_EQ(gdial_util_str_str_hashtable_to_xml_string(nullptr, &len), nullptr); +} + +TEST_F(GDialUtilHashTableTest, ToXml_NullLengthPointerAllowed) { + g_hash_table_insert(ht1, g_strdup("k"), g_strdup("v")); + gchar *s = gdial_util_str_str_hashtable_to_xml_string(ht1, nullptr); + ASSERT_NE(s, nullptr); + EXPECT_NE(g_strstr_len(s, -1, "k=\"v\""), nullptr); + g_free(s); +} + +/* ================================================================== */ +/* gdial_util_str_str_hashtable_dup */ +/* ================================================================== */ + +TEST_F(GDialUtilHashTableTest, Dup_ClonesAllEntries) { + g_hash_table_insert(ht1, g_strdup("a"), g_strdup("1")); + g_hash_table_insert(ht1, g_strdup("b"), g_strdup("2")); + GHashTable *copy = gdial_util_str_str_hashtable_dup(ht1); + ASSERT_NE(copy, nullptr); + EXPECT_EQ(g_hash_table_size(copy), (guint)2); + EXPECT_STREQ((gchar *)g_hash_table_lookup(copy, "a"), "1"); + EXPECT_STREQ((gchar *)g_hash_table_lookup(copy, "b"), "2"); + g_hash_table_destroy(copy); +} + +TEST_F(GDialUtilHashTableTest, Dup_IsDeepCopyNotSamePointer) { + g_hash_table_insert(ht1, g_strdup("key"), g_strdup("val")); + GHashTable *copy = gdial_util_str_str_hashtable_dup(ht1); + ASSERT_NE(copy, nullptr); + EXPECT_NE(copy, ht1); + g_hash_table_destroy(copy); +} + +TEST(GDialUtilTest, Dup_NullReturnsNull) { + EXPECT_EQ(gdial_util_str_str_hashtable_dup(nullptr), nullptr); +} + +/* ================================================================== */ +/* gdial_util_str_str_hashtable_equal */ +/* ================================================================== */ + +TEST_F(GDialUtilHashTableTest, Equal_EmptyTablesAreEqual) { + EXPECT_TRUE(gdial_util_str_str_hashtable_equal(ht1, ht2)); +} + +TEST_F(GDialUtilHashTableTest, Equal_SamePointerIsEqual) { + g_hash_table_insert(ht1, g_strdup("x"), g_strdup("y")); + EXPECT_TRUE(gdial_util_str_str_hashtable_equal(ht1, ht1)); +} + +TEST_F(GDialUtilHashTableTest, Equal_IdenticalContentsAreEqual) { + g_hash_table_insert(ht1, g_strdup("k"), g_strdup("v")); + g_hash_table_insert(ht2, g_strdup("k"), g_strdup("v")); + EXPECT_TRUE(gdial_util_str_str_hashtable_equal(ht1, ht2)); +} + +TEST_F(GDialUtilHashTableTest, Equal_DifferentValuesNotEqual) { + g_hash_table_insert(ht1, g_strdup("k"), g_strdup("v1")); + g_hash_table_insert(ht2, g_strdup("k"), g_strdup("v2")); + EXPECT_FALSE(gdial_util_str_str_hashtable_equal(ht1, ht2)); +} + +TEST_F(GDialUtilHashTableTest, Equal_DifferentSizeNotEqual) { + g_hash_table_insert(ht1, g_strdup("k"), g_strdup("v")); + /* ht2 is empty */ + EXPECT_FALSE(gdial_util_str_str_hashtable_equal(ht1, ht2)); +} + +TEST_F(GDialUtilHashTableTest, Equal_NullValuesAreEqual) { + g_hash_table_insert(ht1, g_strdup("k"), nullptr); + g_hash_table_insert(ht2, g_strdup("k"), nullptr); + EXPECT_TRUE(gdial_util_str_str_hashtable_equal(ht1, ht2)); +} + +TEST_F(GDialUtilHashTableTest, Equal_OneNullValueNotEqual) { + g_hash_table_insert(ht1, g_strdup("k"), nullptr); + g_hash_table_insert(ht2, g_strdup("k"), g_strdup("v")); + EXPECT_FALSE(gdial_util_str_str_hashtable_equal(ht1, ht2)); +} + +TEST_F(GDialUtilHashTableTest, Equal_NullRightNotEqual) { + EXPECT_FALSE(gdial_util_str_str_hashtable_equal(ht1, nullptr)); +} + +TEST_F(GDialUtilHashTableTest, Equal_NullLeftNotEqual) { + EXPECT_FALSE(gdial_util_str_str_hashtable_equal(nullptr, ht2)); +} + +/* ================================================================== */ +/* gdial_util_str_str_hashtable_merge */ +/* ================================================================== */ + +TEST_F(GDialUtilHashTableTest, Merge_AddsSrcKeysToDst) { + /* merge() reuses src key/value pointers inside dst via g_hash_table_replace. + * Use a non-owning src table here to avoid double-free in fixture teardown. */ + GHashTable *src = g_hash_table_new(g_str_hash, g_str_equal); + + g_hash_table_insert(ht1, g_strdup("a"), g_strdup("1")); + g_hash_table_insert(src, g_strdup("b"), g_strdup("2")); + + GHashTable *result = gdial_util_str_str_hashtable_merge(ht1, src); + + EXPECT_EQ(result, ht1); + EXPECT_EQ(g_hash_table_size(ht1), (guint)2); + EXPECT_STREQ((gchar *)g_hash_table_lookup(ht1, "b"), "2"); + + g_hash_table_destroy(src); +} + +TEST_F(GDialUtilHashTableTest, Merge_NullSrcReturnsDstUnchanged) { + g_hash_table_insert(ht1, g_strdup("k"), g_strdup("v")); + GHashTable *result = gdial_util_str_str_hashtable_merge(ht1, nullptr); + EXPECT_EQ(result, ht1); + EXPECT_EQ(g_hash_table_size(ht1), (guint)1); +} + +TEST_F(GDialUtilHashTableTest, Merge_NullDstReturnsNull) { + g_hash_table_insert(ht2, g_strdup("k"), g_strdup("v")); + GHashTable *result = gdial_util_str_str_hashtable_merge(nullptr, ht2); + EXPECT_EQ(result, nullptr); +} + +TEST_F(GDialUtilHashTableTest, Merge_OverwritesExistingKey) { + GHashTable *src = g_hash_table_new(g_str_hash, g_str_equal); + + g_hash_table_insert(ht1, g_strdup("k"), g_strdup("old")); + g_hash_table_insert(src, g_strdup("k"), g_strdup("new")); + + GHashTable *result = gdial_util_str_str_hashtable_merge(ht1, src); + EXPECT_EQ(result, ht1); + EXPECT_STREQ((gchar *)g_hash_table_lookup(ht1, "k"), "new"); + + g_hash_table_destroy(src); +} + diff --git a/tests/Makefile.am b/tests/Makefile.am new file mode 100644 index 00000000..1b70995c --- /dev/null +++ b/tests/Makefile.am @@ -0,0 +1,22 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2019 RDK Management +# +# 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. +########################################################################## + +if ENABLE_L1TESTS +SUBDIRS = L1Tests +endif diff --git a/tests/README.md b/tests/README.md index e25fbe47..a6d5e7eb 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,2 +1,134 @@ # Tests -The test files will be updated to this repo in near future. + +This folder contains the xdialserver test harness and all unit-level (L1) tests. + +## What Is Here + +Current top-level layout: + +- tests/L1Tests: L1 test sources and build recipe +- tests/mocks: shared mock code used by tests +- tests/Makefile.am: autotools entry for test subdirectories +- tests/README.md: this guide + +Inside tests/L1Tests: + +- tests/L1Tests/server: server-layer tests (REST, SSDP, shield, service, app lifecycle) +- tests/L1Tests/plat: platform integration tests for plat APIs and gdial.cpp adapter paths +- tests/L1Tests/utils: utility tests +- tests/L1Tests/stubs: shadow headers and test-specific C/C++ shim headers +- tests/L1Tests/mocks: C mocks compiled into run_L1Tests +- tests/L1Tests/tests: legacy unit file coverage entry points +- tests/L1Tests/test_main.cpp: gtest main +- tests/L1Tests/Makefile.am: source list and compile/link flags + +## How The L1 System Works + +The L1 test binary is a single executable: + +- run_L1Tests + +It links: + +- test files from tests/L1Tests +- selected real implementation files from server and server/plat +- test stubs and mocks that replace external dependencies + +Key behavior: + +1. Real source + selective stubbing +- We compile real modules for behavior coverage, but override dependencies below those modules. +- Example: a real server/plat module can be tested while its OS/backend calls are stubbed. + +2. Include shadowing for external frameworks +- tests/L1Tests/stubs is first on the include path. +- CI generates lightweight wrapper headers there so build-time includes resolve without requiring full external frameworks. + +3. One process, shared globals +- Most L1 tests run in the same process, so static/global state in C modules can leak between tests unless explicitly reset. +- Tests that touch module-level caches must clean up in TearDown. + +## Build And Run Locally + +Typical local flow: + +1. Generate autotools files +- autoreconf -if + +2. Configure with L1 enabled +- ./configure --enable-l1tests + +3. Build +- make -C tests/L1Tests + +4. Run +- ./tests/L1Tests/run_L1Tests + +Optional single-test execution: + +- ./tests/L1Tests/run_L1Tests --gtest_filter=GDialSsdpTest.* +- ./tests/L1Tests/run_L1Tests --gtest_filter=GDialPlatAppTest.* + +## CI Workflow Summary + +The workflow in .github/workflows/L1-tests.yml does the following: + +1. Installs dependencies and builds googletest +2. Generates stub wrapper headers under tests/L1Tests/stubs +3. Builds run_L1Tests using autotools +4. Runs tests normally and under valgrind +5. Publishes test results, valgrind log, and coverage artifacts + +## Adding Or Modifying Tests Safely + +When adding tests: + +1. Place file in the matching area +- server logic: tests/L1Tests/server +- platform logic: tests/L1Tests/plat +- utility helpers: tests/L1Tests/utils + +2. Register file in tests/L1Tests/Makefile.am +- Add it to run_L1Tests_SOURCES or it will not compile in CI. + +3. Isolate global state +- If module under test uses static/global variables, reset via module destroy/reset APIs in TearDown. +- Do not rely on test execution order. + +4. Prefer deterministic async tests +- For GLib/libsoup event paths, avoid timing-sensitive sleeps when possible. +- Use explicit loop-driven completion or cancel/remove handles in tests to avoid race-driven flakes. + +5. Keep assertions aligned with implementation contracts +- Validate against current API behavior and constants from headers. +- If a behavior changed intentionally, update tests and document the expected contract in the test name. + +## Known Pitfalls (Important) + +1. Static cache/state in SSDP tests +- server/gdial-ssdp.c caches dd.xml response and keeps process-global name/model overrides. +- If a test sets manufacturer/model through setter APIs, later tests may observe those values unless reset or overridden. + +2. Async source lifecycle in platform app tests +- server/plat/gdial-plat-app.c async calls use GLib timeout sources. +- Running timer callbacks near teardown can trigger use-after-free races if test cleanup is not deterministic. +- Prefer cancel/remove flow tests over race-prone timer execution unless explicitly testing callback timing. + +3. Header dependency visibility in C++ tests +- Some C headers expose GLib types such as gboolean. +- In C++ test translation units, include glib.h before such headers when needed to avoid type-resolution build breaks. + +## Quick Troubleshooting + +If CI fails but local passes: + +1. Re-run only the failing fixture with gtest_filter. +2. Run under valgrind locally if available. +3. Check for shared static state and missing teardown cleanup. +4. Verify test source is included in tests/L1Tests/Makefile.am. +5. Check .github/workflows/L1-tests.yml for CI-only generated stubs that local build may not have. + +## Maintainer Notes + +- Keep this document updated whenever structure or test harness behavior changes. +- When introducing new shared stubs or wrappers, document where they are generated and why.