/* * 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 "zm_catch2.h" #include "zm_monitor.h" #include #include #include #include namespace { std::string ReadFile(const std::filesystem::path &path) { std::ifstream input(path); REQUIRE(input.is_open()); std::ostringstream buffer; buffer << input.rdbuf(); return buffer.str(); } Monitor::EventAction MakeAction(const std::string &type, int audio_file = -1) { Monitor::EventAction action; action.trigger = Monitor::EventAction::EVENT_START; action.action_type = type; action.target_monitor_id = 2; action.audio_file = audio_file; return action; } } // namespace TEST_CASE("Monitor action type maps onto a control command") { SECTION("every stored action type has a command") { REQUIRE(std::string(Monitor::ActionCommandName("LightOn")) == "lightOn"); REQUIRE(std::string(Monitor::ActionCommandName("LightOff")) == "lightOff"); REQUIRE(std::string(Monitor::ActionCommandName("IndicatorLightOn")) == "indicatorLightOn"); REQUIRE(std::string(Monitor::ActionCommandName("IndicatorLightOff")) == "indicatorLightOff"); REQUIRE(std::string(Monitor::ActionCommandName("AudioPlay")) == "audioPlay"); REQUIRE(std::string(Monitor::ActionCommandName("AudioStop")) == "audioStop"); } SECTION("anything not on the whitelist is refused") { // The mapping is a whitelist precisely so a value that reached the column // by some other route cannot become a method call. REQUIRE(Monitor::ActionCommandName("") == nullptr); REQUIRE(Monitor::ActionCommandName("lightOn") == nullptr); REQUIRE(Monitor::ActionCommandName("reboot") == nullptr); REQUIRE(Monitor::ActionCommandName("AudioPlay; rm -rf /") == nullptr); } } TEST_CASE("Monitor action message is the zmcontrol wire format") { SECTION("a sound carries its file id") { REQUIRE(Monitor::ActionMessage(MakeAction("AudioPlay", 12)) == "{\"command\":\"audioPlay\",\"file\":12}"); } SECTION("file id 0 is a legitimate id, not an absent one") { REQUIRE(Monitor::ActionMessage(MakeAction("AudioPlay", 0)) == "{\"command\":\"audioPlay\",\"file\":0}"); } SECTION("a sound with no file id configured omits the argument") { REQUIRE(Monitor::ActionMessage(MakeAction("AudioPlay")) == "{\"command\":\"audioPlay\"}"); } SECTION("actions that take no file never carry one") { // A row may keep a stale AudioFile after its type is changed; the message // must not pass it to a command that does not understand it. REQUIRE(Monitor::ActionMessage(MakeAction("LightOn", 12)) == "{\"command\":\"lightOn\"}"); REQUIRE(Monitor::ActionMessage(MakeAction("AudioStop", 12)) == "{\"command\":\"audioStop\"}"); } SECTION("an unknown type yields no message at all") { REQUIRE(Monitor::ActionMessage(MakeAction("Nonsense", 1)).empty()); } } TEST_CASE("Monitor action trigger names round trip") { REQUIRE(std::string(Monitor::ActionTriggerName(Monitor::EventAction::EVENT_START)) == "EventStart"); REQUIRE(std::string(Monitor::ActionTriggerName(Monitor::EventAction::EVENT_END)) == "EventEnd"); REQUIRE(std::string(Monitor::ActionTriggerName(Monitor::EventAction::ALARM)) == "Alarm"); REQUIRE(std::string(Monitor::ActionTriggerName(Monitor::EventAction::ALARM_END)) == "AlarmEnd"); REQUIRE(std::string(Monitor::ActionTriggerName(Monitor::EventAction::MANUAL)) == "Manual"); // Every name must round trip through the column, or an action saved by the // editor would load with the wrong trigger and fire at the wrong moment. const auto schema = ReadFile(std::filesystem::path(ZM_SOURCE_DIR) / "db" / "zm_create.sql.in"); const auto enum_start = schema.find("`TriggerOn` enum("); REQUIRE(enum_start != std::string::npos); const auto enum_body = schema.substr(enum_start, schema.find(')', enum_start) - enum_start); for (const auto trigger : {Monitor::EventAction::EVENT_START, Monitor::EventAction::EVENT_END, Monitor::EventAction::ALARM, Monitor::EventAction::ALARM_END, Monitor::EventAction::MANUAL}) { REQUIRE(enum_body.find(std::string("'") + Monitor::ActionTriggerName(trigger) + "'") != std::string::npos); } } TEST_CASE("Monitor action schema") { const auto repo_root = std::filesystem::path(ZM_SOURCE_DIR); SECTION("fresh schema carries DeviceClass and MonitorActions") { const auto schema = ReadFile(repo_root / "db" / "zm_create.sql.in"); REQUIRE(schema.find("`DeviceClass` enum('Camera','Speaker') NOT NULL default 'Camera'") != std::string::npos); REQUIRE(schema.find("CREATE TABLE `MonitorActions`") != std::string::npos); REQUIRE(schema.find("`TargetMonitorId` int(10) unsigned NOT NULL") != std::string::npos); } SECTION("upgrade migration adds both") { const auto migration = ReadFile(repo_root / "db" / "zm_update-1.39.30.sql"); REQUIRE(migration.find("ADD COLUMN `DeviceClass` enum('Camera','Speaker')") != std::string::npos); REQUIRE(migration.find("CREATE TABLE `MonitorActions`") != std::string::npos); // Re-running an upgrade must not duplicate either object. REQUIRE(migration.find("INFORMATION_SCHEMA.COLUMNS") != std::string::npos); REQUIRE(migration.find("INFORMATION_SCHEMA.TABLES") != std::string::npos); } SECTION("the action enum and the C++ whitelist agree") { // The DB enum is the only source of ActionType values, so every one of them // must map to a command or the row would silently never fire. const auto schema = ReadFile(repo_root / "db" / "zm_create.sql.in"); const auto enum_start = schema.find("`ActionType` enum("); REQUIRE(enum_start != std::string::npos); const auto enum_end = schema.find(')', enum_start); const auto enum_body = schema.substr(enum_start, enum_end - enum_start); for (const char *type : {"LightOn", "LightOff", "IndicatorLightOn", "IndicatorLightOff", "AudioPlay", "AudioStop"}) { REQUIRE(enum_body.find(std::string("'") + type + "'") != std::string::npos); REQUIRE(Monitor::ActionCommandName(type) != nullptr); } } }