diff --git a/CMakeLists.txt b/CMakeLists.txt
index 99e19ab4f..fc8454eab 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -134,9 +134,10 @@ mark_as_advanced(
ZM_SYSTEMD
ZM_MANPAGE_DEST_PREFIX)
+option(BUILD_TEST_SUITE "Build the test suite" 0)
option(BUILD_MAN "Build man pages" 1)
-set(ZM_RUNDIR "/var/run/zm" CACHE PATH
+set(ZM_RUNDIR "/var/run/zm" CACHE PATH
"Location of transient process files, default: /var/run/zm")
set(ZM_SOCKDIR "/var/run/zm" CACHE PATH
"Location of Unix domain socket files, default /var/run/zm")
@@ -899,6 +900,16 @@ add_subdirectory(web)
add_subdirectory(misc)
add_subdirectory(onvif)
+if(BUILD_TEST_SUITE)
+ message("Building unit tests: Yes")
+ find_package(Catch2 REQUIRED)
+
+ include(CTest)
+ add_subdirectory(tests)
+else()
+ message("Building unit tests: No (default)")
+endif(BUILD_TEST_SUITE)
+
# Process distro subdirectories
if((ZM_TARGET_DISTRO MATCHES "^el") OR (ZM_TARGET_DISTRO MATCHES "^fc"))
add_subdirectory(distros/redhat)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index c9f11b064..8fa36cf40 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -72,6 +72,11 @@ set(ZM_BIN_SRC_FILES
# A fix for cmake recompiling the source files for every target.
add_library(zm STATIC ${ZM_BIN_SRC_FILES})
+
+target_include_directories(zm
+ PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR})
+
link_directories(libbcrypt)
add_executable(zmc zmc.cpp)
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
new file mode 100644
index 000000000..ba0e46436
--- /dev/null
+++ b/tests/CMakeLists.txt
@@ -0,0 +1,30 @@
+# This file is part of the ZoneMinder Project.
+#
+# This file is free software; as a special exception the author gives
+# unlimited permission to copy and/or distribute it, with or without
+# modifications, as long as this notice is preserved.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY, to the extent permitted by law; without even the
+# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+
+include(Catch)
+
+set(TEST_SOURCES
+ zm_crypt.cpp)
+
+add_executable(tests main.cpp ${TEST_SOURCES})
+
+target_link_libraries(tests
+ PRIVATE
+ zm
+ ${ZM_BIN_LIBS}
+ bcrypt
+ Catch2::Catch2)
+
+target_include_directories(tests
+ PRIVATE
+ ${CMAKE_CURRENT_BINARY_DIR})
+
+catch_discover_tests(tests)
diff --git a/tests/main.cpp b/tests/main.cpp
new file mode 100644
index 000000000..1a00d1291
--- /dev/null
+++ b/tests/main.cpp
@@ -0,0 +1,19 @@
+/*
+ * This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see .
+ */
+
+#define CATCH_CONFIG_MAIN
+#include "catch2/catch.hpp"
diff --git a/tests/zm_crypt.cpp b/tests/zm_crypt.cpp
new file mode 100644
index 000000000..d3a315ef4
--- /dev/null
+++ b/tests/zm_crypt.cpp
@@ -0,0 +1,78 @@
+/*
+ * This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see .
+ */
+
+#include "catch2/catch.hpp"
+
+#include "zm_crypt.h"
+
+TEST_CASE("JWT validation") {
+ std::string key = "testsecret";
+
+ SECTION("Valid token") {
+ std::string token =
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJab25lTWluZGVyIiwidXNlciI6ImpvaG5kb2UiLCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxMjM0fQ.94WPmBAVl_83KCI9B3Jq9sNpoOdi0Hm1dR4sc6MCPUA";
+ std::pair result = verifyToken(token, key);
+
+ REQUIRE(result.first == "johndoe");
+ REQUIRE(result.second == 1234);
+ }
+
+ SECTION("Invalid signature") {
+ std::string token =
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJab25lTWluZGVyIiwidXNlciI6ImpvaG5kb2UiLCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxMjM0fQ.DhviT6RkDLmbXh5F9zM4l0VbWNPCuKptF6fORv1lBlA";
+ std::pair result = verifyToken(token, key);
+
+ REQUIRE(result.first == "");
+ REQUIRE(result.second == 0);
+ }
+
+ SECTION("Missing user claim") {
+ std::string token =
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJab25lTWluZGVyIiwidHlwZSI6ImFjY2VzcyIsImlhdCI6MTIzNH0.mfi3ZHnqUAPUh5ECxDIkAM9WW9a8HbKrP73LC3yYJmw";
+ std::pair result = verifyToken(token, key);
+
+ REQUIRE(result.first == "");
+ REQUIRE(result.second == 0);
+ }
+
+ SECTION("Missing type claim") {
+ std::string token =
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJab25lTWluZGVyIiwidXNlciI6ImpvaG5kb2UiLCJpYXQiOjEyMzR9.D4Irs1gHfzO4psRY2xsOdClTg-Sp1kM__mmfNLs7CII";
+ std::pair result = verifyToken(token, key);
+
+ REQUIRE(result.first == "");
+ REQUIRE(result.second == 0);
+ }
+
+ SECTION("Wrong type claim") {
+ std::string token =
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJab25lTWluZGVyIiwidXNlciI6ImpvaG5kb2UiLCJ0eXBlIjoid3JvbmciLCJpYXQiOjEyMzR9.I1Gd50J6mck05vzc_kzjaH4RNjLBaFGpOnie6-PbX28";
+ std::pair result = verifyToken(token, key);
+
+ REQUIRE(result.first == "");
+ REQUIRE(result.second == 0);
+ }
+
+ SECTION("Missing iat claim") {
+ std::string token =
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJab25lTWluZGVyIiwidXNlciI6ImpvaG5kb2UiLCJ0eXBlIjoid3JvbmcifQ.8iUFOUKJAK5vU8JWKm8D0EOEhm1rJoIulCO11O_Tsp0";
+ std::pair result = verifyToken(token, key);
+
+ REQUIRE(result.first == "");
+ REQUIRE(result.second == 0);
+ }
+}