forked from ZoneMinder/zoneminder
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathzm_onvif_renewal.cpp
More file actions
354 lines (296 loc) · 13 KB
/
zm_onvif_renewal.cpp
File metadata and controls
354 lines (296 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "zm_catch2.h"
#include "zm_time.h"
#include <chrono>
#include <string>
#include <unordered_map>
// Test the ONVIF subscription renewal timing logic
TEST_CASE("ONVIF Subscription Renewal Timing") {
SECTION("Calculate renewal time from termination time") {
// Simulate a termination time 60 seconds from now
auto now = std::chrono::system_clock::now();
time_t termination_time_t = std::chrono::system_clock::to_time_t(
now + std::chrono::seconds(60));
// Convert to SystemTimePoint
SystemTimePoint termination_time = std::chrono::system_clock::from_time_t(termination_time_t);
// Calculate renewal time (10 seconds before termination)
SystemTimePoint renewal_time = termination_time - std::chrono::seconds(10);
// Check that renewal time is 50 seconds from now (60 - 10)
auto seconds_until_renewal = std::chrono::duration_cast<std::chrono::seconds>(
renewal_time - now).count();
// Allow 1 second tolerance for test execution time
REQUIRE(seconds_until_renewal >= 49);
REQUIRE(seconds_until_renewal <= 51);
}
SECTION("Check if renewal is needed - not yet time") {
auto now = std::chrono::system_clock::now();
// Renewal time is 30 seconds in the future
SystemTimePoint renewal_time = now + std::chrono::seconds(30);
// Should not need renewal yet
bool renewal_needed = (now >= renewal_time);
REQUIRE_FALSE(renewal_needed);
}
SECTION("Check if renewal is needed - time has come") {
auto now = std::chrono::system_clock::now();
// Renewal time was 1 second ago
SystemTimePoint renewal_time = now - std::chrono::seconds(1);
// Should need renewal
bool renewal_needed = (now >= renewal_time);
REQUIRE(renewal_needed);
}
SECTION("Check if renewal times are uninitialized") {
// Default constructed SystemTimePoint has epoch (0)
SystemTimePoint uninitialized_time;
bool is_uninitialized = (uninitialized_time.time_since_epoch().count() == 0);
REQUIRE(is_uninitialized);
}
SECTION("Time conversion round-trip") {
// Test that time_t -> SystemTimePoint -> time_t conversion is accurate
time_t original_time = 1704844800; // 2024-01-10 00:00:00 UTC
SystemTimePoint tp = std::chrono::system_clock::from_time_t(original_time);
time_t converted_time = std::chrono::system_clock::to_time_t(tp);
REQUIRE(original_time == converted_time);
}
}
// Test the ONVIF subscription cleanup logic
// Note: These tests document the expected behavior. Full integration testing
// with actual ONVIF cameras would require a mock SOAP server.
TEST_CASE("ONVIF Subscription Cleanup Logic") {
SECTION("Cleanup should prevent subscription leaks on renewal failure") {
// When Renew() fails (non-ActionNotSupported error), cleanup_subscription()
// should be called to unsubscribe from the camera before returning false.
// This prevents orphaned subscriptions from accumulating on the camera.
//
// Expected behavior verified in zm_monitor_onvif.cpp:
// 1. Renew() calls proxyEvent.Renew()
// 2. If result != SOAP_OK and error != 12 (ActionNotSupported):
// a. Log the renewal failure
// b. Call cleanup_subscription() to unsubscribe
// c. Set healthy = false
// d. Return false
REQUIRE(true); // Behavior verified through code inspection
}
SECTION("Cleanup should be called before creating new subscription in start()") {
// When start() is called and soap != nullptr (from previous failed attempt),
// cleanup_subscription() should be called before creating a new subscription.
// This ensures any stale subscription is cleaned up first.
//
// Expected behavior verified in zm_monitor_onvif.cpp:
// 1. start() checks if soap != nullptr at beginning
// 2. If true:
// a. Log that existing soap context was found
// b. Call cleanup_subscription() to unsubscribe from stale subscription
// c. Clean up the old soap context (disable logging, destroy, end, free)
// d. Set soap = nullptr
// 3. Then proceed with normal subscription creation
REQUIRE(true); // Behavior verified through code inspection
}
SECTION("Destructor should log unsubscribe failures") {
// The destructor should check the result of Unsubscribe() and log warnings
// if it fails, helping identify cameras that don't properly handle cleanup.
//
// Expected behavior verified in zm_monitor_onvif.cpp:
// 1. Destructor attempts to unsubscribe
// 2. Captures result from proxyEvent.Unsubscribe()
// 3. If result != SOAP_OK:
// a. Log a Warning with error details
// b. Indicate that subscription may remain on camera
// 4. If result == SOAP_OK:
// a. Log Debug message confirming successful unsubscribe
REQUIRE(true); // Behavior verified through code inspection
}
SECTION("WS-Addressing failure in Renew should trigger cleanup") {
// If do_wsa_request() fails during Renew(), cleanup_subscription() should
// be called before returning false to prevent subscription leaks.
//
// Expected behavior verified in zm_monitor_onvif.cpp:
// 1. Renew() calls do_wsa_request() if WS-Addressing is enabled
// 2. If do_wsa_request() returns false:
// a. Log that WS-Addressing setup failed
// b. Call cleanup_subscription()
// c. Set healthy = false
// d. Return false
REQUIRE(true); // Behavior verified through code inspection
}
}
// Test the ISO 8601 absolute time formatting for ONVIF renewal requests
TEST_CASE("ONVIF Absolute Time Formatting") {
SECTION("Format known timestamp as ISO 8601") {
// Test with known timestamp: 2024-01-13 13:14:56 UTC
time_t test_time = 1705151696; // 2024-01-13 13:14:56 UTC
std::string result = format_absolute_time_iso8601(test_time);
// Should be formatted as ISO 8601 with .000Z suffix
REQUIRE(result == "2024-01-13T13:14:56.000Z");
}
SECTION("Format current time as ISO 8601") {
time_t now = time(nullptr);
std::string result = format_absolute_time_iso8601(now);
// Should not be empty
REQUIRE_FALSE(result.empty());
// Should have expected format with 'T' separator and 'Z' suffix
REQUIRE(result.find('T') != std::string::npos);
REQUIRE(result.find('Z') != std::string::npos);
REQUIRE(result.back() == 'Z');
// Should have the correct length (YYYY-MM-DDTHH:MM:SS.000Z = 24 characters)
REQUIRE(result.length() == 24);
}
SECTION("Format future time for renewal") {
// Simulate renewal: current time + 60 seconds
time_t now = time(nullptr);
time_t renewal_time = now + 60;
std::string result = format_absolute_time_iso8601(renewal_time);
// Should not be empty
REQUIRE_FALSE(result.empty());
// Should have expected format
REQUIRE(result.find('T') != std::string::npos);
REQUIRE(result.find('Z') != std::string::npos);
REQUIRE(result.length() == 24);
}
SECTION("Verify ISO 8601 format components") {
time_t test_time = 1705151696; // 2024-01-13 13:14:56 UTC
std::string result = format_absolute_time_iso8601(test_time);
// Check year
REQUIRE(result.substr(0, 4) == "2024");
// Check separators
REQUIRE(result[4] == '-'); // After year
REQUIRE(result[7] == '-'); // After month
REQUIRE(result[10] == 'T'); // Date/time separator
REQUIRE(result[13] == ':'); // After hour
REQUIRE(result[16] == ':'); // After minute
REQUIRE(result[19] == '.'); // After second
REQUIRE(result[23] == 'Z'); // UTC indicator
}
}
// Standalone AlarmEntry struct matching the one in zm_monitor_onvif.h.
// We replicate it here so tests don't depend on gSOAP headers.
namespace onvif_test {
struct AlarmEntry {
std::string value;
SystemTimePoint termination_time;
};
using AlarmMap = std::unordered_map<std::string, AlarmEntry>;
// Mirror of ONVIF::expire_stale_alarms logic for unit testing.
// Returns true if the map became empty (caller should setAlarmed(false)).
bool expire_stale_alarms(AlarmMap &alarms, const SystemTimePoint &now) {
auto it = alarms.begin();
while (it != alarms.end()) {
// Skip entries with no termination time set (epoch = uninitialized)
if (it->second.termination_time.time_since_epoch().count() == 0) {
++it;
continue;
}
if (it->second.termination_time <= now) {
it = alarms.erase(it);
} else {
++it;
}
}
return alarms.empty();
}
} // namespace onvif_test
// Test per-topic TerminationTime alarm expiry logic
TEST_CASE("ONVIF Per-Topic Alarm Expiry") {
using namespace onvif_test;
auto now = std::chrono::system_clock::now();
SECTION("Expired alarms are removed by sweep") {
AlarmMap alarms;
// Alarm with TerminationTime 10 seconds in the past
alarms["PeopleDetect"] = AlarmEntry{"true", now - std::chrono::seconds(10)};
bool empty = expire_stale_alarms(alarms, now);
REQUIRE(alarms.empty());
REQUIRE(empty);
}
SECTION("Future alarms are retained by sweep") {
AlarmMap alarms;
// Alarm with TerminationTime 60 seconds in the future
alarms["MotionAlarm"] = AlarmEntry{"true", now + std::chrono::seconds(60)};
bool empty = expire_stale_alarms(alarms, now);
REQUIRE(alarms.size() == 1);
REQUIRE_FALSE(empty);
}
SECTION("Mixed expired and future alarms") {
AlarmMap alarms;
alarms["PeopleDetect"] = AlarmEntry{"true", now - std::chrono::seconds(10)};
alarms["MotionAlarm"] = AlarmEntry{"true", now + std::chrono::seconds(60)};
bool empty = expire_stale_alarms(alarms, now);
REQUIRE(alarms.size() == 1);
REQUIRE(alarms.count("MotionAlarm") == 1);
REQUIRE(alarms.count("PeopleDetect") == 0);
REQUIRE_FALSE(empty);
}
SECTION("Re-triggering an alarm updates its TerminationTime") {
AlarmMap alarms;
// Initial alarm with TerminationTime 5 seconds from now
SystemTimePoint initial_term = now + std::chrono::seconds(5);
alarms["PeopleDetect"] = AlarmEntry{"true", initial_term};
// Simulate re-trigger with new TerminationTime 65 seconds from now
SystemTimePoint new_term = now + std::chrono::seconds(65);
alarms["PeopleDetect"] = AlarmEntry{"true", new_term};
// Sweep at now+10s - alarm should NOT be expired because it was refreshed
SystemTimePoint sweep_time = now + std::chrono::seconds(10);
bool empty = expire_stale_alarms(alarms, sweep_time);
REQUIRE(alarms.size() == 1);
REQUIRE_FALSE(empty);
// Verify the termination time was updated
REQUIRE(alarms["PeopleDetect"].termination_time == new_term);
}
SECTION("Alarms with epoch termination time (uninitialized) are not expired") {
AlarmMap alarms;
// Alarm with default-constructed (epoch) termination time
alarms["SomeAlarm"] = AlarmEntry{"true", SystemTimePoint{}};
bool empty = expire_stale_alarms(alarms, now);
REQUIRE(alarms.size() == 1);
REQUIRE_FALSE(empty);
}
SECTION("TerminationTime exactly equal to now is expired") {
AlarmMap alarms;
alarms["PeopleDetect"] = AlarmEntry{"true", now};
bool empty = expire_stale_alarms(alarms, now);
REQUIRE(alarms.empty());
REQUIRE(empty);
}
SECTION("Multiple expired alarms are all removed") {
AlarmMap alarms;
alarms["PeopleDetect"] = AlarmEntry{"true", now - std::chrono::seconds(30)};
alarms["VehicleDetect"] = AlarmEntry{"true", now - std::chrono::seconds(20)};
alarms["DogCatDetect"] = AlarmEntry{"true", now - std::chrono::seconds(10)};
bool empty = expire_stale_alarms(alarms, now);
REQUIRE(alarms.empty());
REQUIRE(empty);
}
SECTION("Empty alarms map is handled gracefully") {
AlarmMap alarms;
bool empty = expire_stale_alarms(alarms, now);
REQUIRE(empty);
}
SECTION("AlarmEntry stores value correctly") {
AlarmEntry entry{"true", now + std::chrono::seconds(60)};
REQUIRE(entry.value == "true");
AlarmEntry entry2{"false", now};
REQUIRE(entry2.value == "false");
}
SECTION("Alarm value accessible via map for SetNoteSet") {
AlarmMap alarms;
alarms["MyRuleDetector/PeopleDetect"] = AlarmEntry{"true", now + std::chrono::seconds(60)};
// Simulate SetNoteSet logic: iterate and access .value
for (auto it = alarms.begin(); it != alarms.end(); ++it) {
std::string note = it->first + "/" + it->second.value;
REQUIRE(note == "MyRuleDetector/PeopleDetect/true");
}
}
}