pumaguard 21.post29__py3-none-any.whl → 21.post83__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. pumaguard/presets.py +1 -0
  2. pumaguard/pumaguard-ui/.last_build_id +1 -1
  3. pumaguard/pumaguard-ui/assets/NOTICES +621 -71
  4. pumaguard/pumaguard-ui/assets/fonts/MaterialIcons-Regular.otf +0 -0
  5. pumaguard/pumaguard-ui/flutter_bootstrap.js +1 -1
  6. pumaguard/pumaguard-ui/main.dart.js +28869 -28787
  7. pumaguard/web_routes/dhcp.py +311 -54
  8. pumaguard/web_routes/diagnostics.py +6 -0
  9. pumaguard/web_routes/settings.py +13 -0
  10. pumaguard/web_ui.py +29 -0
  11. {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/METADATA +1 -1
  12. pumaguard-21.post83.dist-info/RECORD +254 -0
  13. pumaguard-ui/.gitignore +48 -0
  14. pumaguard-ui/.metadata +45 -0
  15. pumaguard-ui/API_REFERENCE.md +717 -0
  16. pumaguard-ui/LICENSE +201 -0
  17. pumaguard-ui/Makefile +36 -0
  18. pumaguard-ui/README.md +371 -0
  19. pumaguard-ui/UI_DEVELOPMENT_CONTEXT.md +427 -0
  20. pumaguard-ui/analysis_options.yaml +28 -0
  21. pumaguard-ui/android/.gitignore +14 -0
  22. pumaguard-ui/android/app/build.gradle.kts +44 -0
  23. pumaguard-ui/android/app/src/debug/AndroidManifest.xml +7 -0
  24. pumaguard-ui/android/app/src/main/AndroidManifest.xml +45 -0
  25. pumaguard-ui/android/app/src/main/kotlin/com/example/pumaguard_ui/MainActivity.kt +5 -0
  26. pumaguard-ui/android/app/src/main/res/drawable/launch_background.xml +12 -0
  27. pumaguard-ui/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  28. pumaguard-ui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  29. pumaguard-ui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  30. pumaguard-ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  31. pumaguard-ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  32. pumaguard-ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  33. pumaguard-ui/android/app/src/main/res/values/styles.xml +18 -0
  34. pumaguard-ui/android/app/src/main/res/values-night/styles.xml +18 -0
  35. pumaguard-ui/android/app/src/profile/AndroidManifest.xml +7 -0
  36. pumaguard-ui/android/build.gradle.kts +24 -0
  37. pumaguard-ui/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  38. pumaguard-ui/android/gradle.properties +2 -0
  39. pumaguard-ui/android/settings.gradle.kts +26 -0
  40. pumaguard-ui/fonts/README.md +38 -0
  41. pumaguard-ui/fonts/Roboto-Bold.ttf +0 -0
  42. pumaguard-ui/fonts/Roboto-Light.ttf +0 -0
  43. pumaguard-ui/fonts/Roboto-Medium.ttf +0 -0
  44. pumaguard-ui/fonts/Roboto-Regular.ttf +0 -0
  45. pumaguard-ui/fonts/RobotoMono-Bold.ttf +0 -0
  46. pumaguard-ui/fonts/RobotoMono-Medium.ttf +0 -0
  47. pumaguard-ui/fonts/RobotoMono-Regular.ttf +0 -0
  48. pumaguard-ui/fonts/download_fonts.sh +76 -0
  49. pumaguard-ui/ios/.gitignore +34 -0
  50. pumaguard-ui/ios/Flutter/AppFrameworkInfo.plist +26 -0
  51. pumaguard-ui/ios/Flutter/Debug.xcconfig +1 -0
  52. pumaguard-ui/ios/Flutter/Release.xcconfig +1 -0
  53. pumaguard-ui/ios/Runner/AppDelegate.swift +13 -0
  54. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
  55. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
  56. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
  57. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
  58. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
  59. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
  60. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
  61. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
  62. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
  63. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
  64. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
  65. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
  66. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
  67. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
  68. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
  69. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
  70. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +23 -0
  71. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
  72. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
  73. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
  74. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +5 -0
  75. pumaguard-ui/ios/Runner/Base.lproj/LaunchScreen.storyboard +37 -0
  76. pumaguard-ui/ios/Runner/Base.lproj/Main.storyboard +26 -0
  77. pumaguard-ui/ios/Runner/Info.plist +49 -0
  78. pumaguard-ui/ios/Runner/Runner-Bridging-Header.h +1 -0
  79. pumaguard-ui/ios/Runner.xcodeproj/project.pbxproj +616 -0
  80. pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  81. pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  82. pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  83. pumaguard-ui/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +101 -0
  84. pumaguard-ui/ios/Runner.xcworkspace/contents.xcworkspacedata +7 -0
  85. pumaguard-ui/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  86. pumaguard-ui/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  87. pumaguard-ui/ios/RunnerTests/RunnerTests.swift +12 -0
  88. pumaguard-ui/lib/main.dart +56 -0
  89. pumaguard-ui/lib/models/camera.dart +45 -0
  90. pumaguard-ui/lib/models/plug.dart +45 -0
  91. pumaguard-ui/lib/models/settings.dart +112 -0
  92. pumaguard-ui/lib/models/status.dart +58 -0
  93. pumaguard-ui/lib/screens/directories_screen.dart +319 -0
  94. pumaguard-ui/lib/screens/home_screen.dart +545 -0
  95. pumaguard-ui/lib/screens/image_browser_screen.dart +1248 -0
  96. pumaguard-ui/lib/screens/server_discovery_screen.dart +390 -0
  97. pumaguard-ui/lib/screens/settings_screen.dart +1162 -0
  98. pumaguard-ui/lib/screens/wifi_settings_screen.dart +671 -0
  99. pumaguard-ui/lib/services/api_service.dart +717 -0
  100. pumaguard-ui/lib/services/camera_events_service.dart +195 -0
  101. pumaguard-ui/lib/services/mdns_service.dart +4 -0
  102. pumaguard-ui/lib/services/mdns_service_impl.dart +282 -0
  103. pumaguard-ui/lib/services/mdns_service_io.dart +1 -0
  104. pumaguard-ui/lib/services/mdns_service_web.dart +106 -0
  105. pumaguard-ui/lib/utils/download_helper.dart +2 -0
  106. pumaguard-ui/lib/utils/download_helper_stub.dart +6 -0
  107. pumaguard-ui/lib/utils/download_helper_web.dart +14 -0
  108. pumaguard-ui/lib/utils/platform_url.dart +10 -0
  109. pumaguard-ui/lib/utils/platform_url_stub.dart +11 -0
  110. pumaguard-ui/lib/utils/platform_url_web.dart +16 -0
  111. pumaguard-ui/linux/.gitignore +1 -0
  112. pumaguard-ui/linux/CMakeLists.txt +128 -0
  113. pumaguard-ui/linux/flutter/CMakeLists.txt +88 -0
  114. pumaguard-ui/linux/flutter/generated_plugin_registrant.cc +15 -0
  115. pumaguard-ui/linux/flutter/generated_plugin_registrant.h +15 -0
  116. pumaguard-ui/linux/flutter/generated_plugins.cmake +24 -0
  117. pumaguard-ui/linux/runner/CMakeLists.txt +26 -0
  118. pumaguard-ui/linux/runner/main.cc +6 -0
  119. pumaguard-ui/linux/runner/my_application.cc +148 -0
  120. pumaguard-ui/linux/runner/my_application.h +21 -0
  121. pumaguard-ui/macos/.gitignore +7 -0
  122. pumaguard-ui/macos/Flutter/Flutter-Debug.xcconfig +1 -0
  123. pumaguard-ui/macos/Flutter/Flutter-Release.xcconfig +1 -0
  124. pumaguard-ui/macos/Flutter/GeneratedPluginRegistrant.swift +16 -0
  125. pumaguard-ui/macos/Runner/AppDelegate.swift +13 -0
  126. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  127. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  128. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  129. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  130. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  131. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  132. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  133. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  134. pumaguard-ui/macos/Runner/Base.lproj/MainMenu.xib +343 -0
  135. pumaguard-ui/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  136. pumaguard-ui/macos/Runner/Configs/Debug.xcconfig +2 -0
  137. pumaguard-ui/macos/Runner/Configs/Release.xcconfig +2 -0
  138. pumaguard-ui/macos/Runner/Configs/Warnings.xcconfig +13 -0
  139. pumaguard-ui/macos/Runner/DebugProfile.entitlements +12 -0
  140. pumaguard-ui/macos/Runner/Info.plist +32 -0
  141. pumaguard-ui/macos/Runner/MainFlutterWindow.swift +15 -0
  142. pumaguard-ui/macos/Runner/Release.entitlements +8 -0
  143. pumaguard-ui/macos/Runner.xcodeproj/project.pbxproj +705 -0
  144. pumaguard-ui/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  145. pumaguard-ui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  146. pumaguard-ui/macos/Runner.xcworkspace/contents.xcworkspacedata +7 -0
  147. pumaguard-ui/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  148. pumaguard-ui/macos/RunnerTests/RunnerTests.swift +12 -0
  149. pumaguard-ui/pubspec.lock +882 -0
  150. pumaguard-ui/pubspec.yaml +125 -0
  151. pumaguard-ui/test/models/camera_test.dart +515 -0
  152. pumaguard-ui/test/models/plug_test.dart +499 -0
  153. pumaguard-ui/test/models/settings_test.dart +903 -0
  154. pumaguard-ui/test/models/status_test.dart +707 -0
  155. pumaguard-ui/test/screens/image_browser_grouping_test.dart +555 -0
  156. pumaguard-ui/test/services/api_service_cameras_test.dart +580 -0
  157. pumaguard-ui/test/services/api_service_image_browser_test.dart +512 -0
  158. pumaguard-ui/test/widget_test.dart.skip +38 -0
  159. pumaguard-ui/web/favicon.png +0 -0
  160. pumaguard-ui/web/icons/Icon-192.png +0 -0
  161. pumaguard-ui/web/icons/Icon-512.png +0 -0
  162. pumaguard-ui/web/icons/Icon-maskable-192.png +0 -0
  163. pumaguard-ui/web/icons/Icon-maskable-512.png +0 -0
  164. pumaguard-ui/web/index.html +38 -0
  165. pumaguard-ui/web/manifest.json +35 -0
  166. pumaguard-ui/windows/.gitignore +17 -0
  167. pumaguard-ui/windows/CMakeLists.txt +108 -0
  168. pumaguard-ui/windows/flutter/CMakeLists.txt +109 -0
  169. pumaguard-ui/windows/flutter/generated_plugin_registrant.cc +14 -0
  170. pumaguard-ui/windows/flutter/generated_plugin_registrant.h +15 -0
  171. pumaguard-ui/windows/flutter/generated_plugins.cmake +24 -0
  172. pumaguard-ui/windows/runner/CMakeLists.txt +40 -0
  173. pumaguard-ui/windows/runner/Runner.rc +121 -0
  174. pumaguard-ui/windows/runner/flutter_window.cpp +71 -0
  175. pumaguard-ui/windows/runner/flutter_window.h +33 -0
  176. pumaguard-ui/windows/runner/main.cpp +43 -0
  177. pumaguard-ui/windows/runner/resource.h +16 -0
  178. pumaguard-ui/windows/runner/resources/app_icon.ico +0 -0
  179. pumaguard-ui/windows/runner/runner.exe.manifest +14 -0
  180. pumaguard-ui/windows/runner/utils.cpp +65 -0
  181. pumaguard-ui/windows/runner/utils.h +19 -0
  182. pumaguard-ui/windows/runner/win32_window.cpp +288 -0
  183. pumaguard-ui/windows/runner/win32_window.h +102 -0
  184. pumaguard-21.post29.dist-info/RECORD +0 -83
  185. {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/WHEEL +0 -0
  186. {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/entry_points.txt +0 -0
  187. {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/licenses/LICENSE +0 -0
  188. {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,580 @@
1
+ // Unit tests for camera APIs in ApiService
2
+ // Tests camera data transformation and parsing logic
3
+
4
+ import 'package:flutter_test/flutter_test.dart';
5
+ import 'package:pumaguard_ui/services/api_service.dart';
6
+ import 'package:pumaguard_ui/models/camera.dart';
7
+
8
+ void main() {
9
+ group('ApiService Camera Tests', () {
10
+ late ApiService apiService;
11
+
12
+ setUp(() {
13
+ apiService = ApiService(baseUrl: 'http://localhost:5000');
14
+ });
15
+
16
+ group('API URL Construction', () {
17
+ test('constructs correct camera list API URL', () {
18
+ final url = apiService.getApiUrl('/api/dhcp/cameras');
19
+ expect(url, 'http://localhost:5000/api/dhcp/cameras');
20
+ });
21
+
22
+ test('constructs correct camera detail API URL', () {
23
+ final url = apiService.getApiUrl('/api/dhcp/cameras/aa:bb:cc:dd:ee:ff');
24
+ expect(url, 'http://localhost:5000/api/dhcp/cameras/aa:bb:cc:dd:ee:ff');
25
+ });
26
+
27
+ test('handles base URL without trailing slash', () {
28
+ final service = ApiService(baseUrl: 'http://example.com');
29
+ final url = service.getApiUrl('/api/dhcp/cameras');
30
+ expect(url, 'http://example.com/api/dhcp/cameras');
31
+ });
32
+
33
+ test('handles base URL with trailing slash', () {
34
+ final service = ApiService(baseUrl: 'http://example.com/');
35
+ final url = service.getApiUrl('/api/dhcp/cameras');
36
+ expect(url, 'http://example.com/api/dhcp/cameras');
37
+ });
38
+ });
39
+
40
+ group('Camera Response Parsing', () {
41
+ test('parses empty camera list response', () {
42
+ final mockResponse = {'cameras': [], 'count': 0};
43
+
44
+ final camerasList = mockResponse['cameras'] as List;
45
+ final cameras = camerasList
46
+ .map((json) => Camera.fromJson(json as Map<String, dynamic>))
47
+ .toList();
48
+
49
+ expect(cameras, isEmpty);
50
+ expect(cameras.length, 0);
51
+ });
52
+
53
+ test('parses single camera response', () {
54
+ final mockResponse = {
55
+ 'cameras': [
56
+ {
57
+ 'hostname': 'Microseven-Cam1',
58
+ 'ip_address': '192.168.52.101',
59
+ 'mac_address': 'aa:bb:cc:dd:ee:01',
60
+ 'last_seen': '2024-01-15T10:30:00Z',
61
+ 'status': 'connected',
62
+ },
63
+ ],
64
+ 'count': 1,
65
+ };
66
+
67
+ final camerasList = mockResponse['cameras'] as List;
68
+ final cameras = camerasList
69
+ .map((json) => Camera.fromJson(json as Map<String, dynamic>))
70
+ .toList();
71
+
72
+ expect(cameras.length, 1);
73
+ expect(cameras[0].hostname, 'Microseven-Cam1');
74
+ expect(cameras[0].ipAddress, '192.168.52.101');
75
+ expect(cameras[0].macAddress, 'aa:bb:cc:dd:ee:01');
76
+ expect(cameras[0].status, 'connected');
77
+ expect(cameras[0].isConnected, true);
78
+ });
79
+
80
+ test('parses multiple cameras response', () {
81
+ final mockResponse = {
82
+ 'cameras': [
83
+ {
84
+ 'hostname': 'Microseven-Cam1',
85
+ 'ip_address': '192.168.52.101',
86
+ 'mac_address': 'aa:bb:cc:dd:ee:01',
87
+ 'last_seen': '2024-01-15T10:30:00Z',
88
+ 'status': 'connected',
89
+ },
90
+ {
91
+ 'hostname': 'Microseven-Cam2',
92
+ 'ip_address': '192.168.52.102',
93
+ 'mac_address': 'aa:bb:cc:dd:ee:02',
94
+ 'last_seen': '2024-01-15T10:35:00Z',
95
+ 'status': 'disconnected',
96
+ },
97
+ ],
98
+ 'count': 2,
99
+ };
100
+
101
+ final camerasList = mockResponse['cameras'] as List;
102
+ final cameras = camerasList
103
+ .map((json) => Camera.fromJson(json as Map<String, dynamic>))
104
+ .toList();
105
+
106
+ expect(cameras.length, 2);
107
+ expect(cameras[0].hostname, 'Microseven-Cam1');
108
+ expect(cameras[0].status, 'connected');
109
+ expect(cameras[0].isConnected, true);
110
+
111
+ expect(cameras[1].hostname, 'Microseven-Cam2');
112
+ expect(cameras[1].status, 'disconnected');
113
+ expect(cameras[1].isConnected, false);
114
+ });
115
+
116
+ test('handles camera with missing optional fields', () {
117
+ final json = {
118
+ 'hostname': '',
119
+ 'ip_address': '192.168.52.100',
120
+ 'mac_address': 'aa:bb:cc:dd:ee:ff',
121
+ 'last_seen': '',
122
+ 'status': 'unknown',
123
+ };
124
+
125
+ final camera = Camera.fromJson(json);
126
+
127
+ expect(camera.hostname, '');
128
+ expect(camera.ipAddress, '192.168.52.100');
129
+ expect(camera.macAddress, 'aa:bb:cc:dd:ee:ff');
130
+ expect(camera.lastSeen, '');
131
+ expect(camera.status, 'unknown');
132
+ expect(camera.isConnected, false);
133
+ });
134
+ });
135
+
136
+ group('Camera Model Integration', () {
137
+ test('fromJson creates valid Camera object', () {
138
+ final json = {
139
+ 'hostname': 'TestCamera',
140
+ 'ip_address': '192.168.52.100',
141
+ 'mac_address': 'aa:bb:cc:dd:ee:ff',
142
+ 'last_seen': '2024-01-15T10:30:00Z',
143
+ 'status': 'connected',
144
+ };
145
+
146
+ final camera = Camera.fromJson(json);
147
+
148
+ expect(camera.hostname, 'TestCamera');
149
+ expect(camera.ipAddress, '192.168.52.100');
150
+ expect(camera.macAddress, 'aa:bb:cc:dd:ee:ff');
151
+ expect(camera.lastSeen, '2024-01-15T10:30:00Z');
152
+ expect(camera.status, 'connected');
153
+ });
154
+
155
+ test('toJson creates valid JSON object', () {
156
+ final camera = Camera(
157
+ hostname: 'TestCamera',
158
+ ipAddress: '192.168.52.100',
159
+ macAddress: 'aa:bb:cc:dd:ee:ff',
160
+ lastSeen: '2024-01-15T10:30:00Z',
161
+ status: 'connected',
162
+ );
163
+
164
+ final json = camera.toJson();
165
+
166
+ expect(json['hostname'], 'TestCamera');
167
+ expect(json['ip_address'], '192.168.52.100');
168
+ expect(json['mac_address'], 'aa:bb:cc:dd:ee:ff');
169
+ expect(json['last_seen'], '2024-01-15T10:30:00Z');
170
+ expect(json['status'], 'connected');
171
+ });
172
+
173
+ test('isConnected returns true for connected status', () {
174
+ final camera = Camera(
175
+ hostname: 'TestCamera',
176
+ ipAddress: '192.168.52.100',
177
+ macAddress: 'aa:bb:cc:dd:ee:ff',
178
+ lastSeen: '2024-01-15T10:30:00Z',
179
+ status: 'connected',
180
+ );
181
+
182
+ expect(camera.isConnected, true);
183
+ });
184
+
185
+ test('isConnected returns false for disconnected status', () {
186
+ final camera = Camera(
187
+ hostname: 'TestCamera',
188
+ ipAddress: '192.168.52.100',
189
+ macAddress: 'aa:bb:cc:dd:ee:ff',
190
+ lastSeen: '2024-01-15T10:30:00Z',
191
+ status: 'disconnected',
192
+ );
193
+
194
+ expect(camera.isConnected, false);
195
+ });
196
+
197
+ test('isConnected returns false for unknown status', () {
198
+ final camera = Camera(
199
+ hostname: 'TestCamera',
200
+ ipAddress: '192.168.52.100',
201
+ macAddress: 'aa:bb:cc:dd:ee:ff',
202
+ lastSeen: '2024-01-15T10:30:00Z',
203
+ status: 'unknown',
204
+ );
205
+
206
+ expect(camera.isConnected, false);
207
+ });
208
+
209
+ test('displayName returns hostname when available', () {
210
+ final camera = Camera(
211
+ hostname: 'MyCoolCamera',
212
+ ipAddress: '192.168.52.100',
213
+ macAddress: 'aa:bb:cc:dd:ee:ff',
214
+ lastSeen: '2024-01-15T10:30:00Z',
215
+ status: 'connected',
216
+ );
217
+
218
+ expect(camera.displayName, 'MyCoolCamera');
219
+ });
220
+
221
+ test('displayName returns IP when hostname is empty', () {
222
+ final camera = Camera(
223
+ hostname: '',
224
+ ipAddress: '192.168.52.100',
225
+ macAddress: 'aa:bb:cc:dd:ee:ff',
226
+ lastSeen: '2024-01-15T10:30:00Z',
227
+ status: 'connected',
228
+ );
229
+
230
+ expect(camera.displayName, '192.168.52.100');
231
+ });
232
+
233
+ test('cameraUrl returns IP address', () {
234
+ final camera = Camera(
235
+ hostname: 'TestCamera',
236
+ ipAddress: '192.168.52.100',
237
+ macAddress: 'aa:bb:cc:dd:ee:ff',
238
+ lastSeen: '2024-01-15T10:30:00Z',
239
+ status: 'connected',
240
+ );
241
+
242
+ expect(camera.cameraUrl, '192.168.52.100');
243
+ });
244
+
245
+ test('cameraUrl returns empty string when IP is empty', () {
246
+ final camera = Camera(
247
+ hostname: 'TestCamera',
248
+ ipAddress: '',
249
+ macAddress: 'aa:bb:cc:dd:ee:ff',
250
+ lastSeen: '2024-01-15T10:30:00Z',
251
+ status: 'connected',
252
+ );
253
+
254
+ expect(camera.cameraUrl, '');
255
+ });
256
+ });
257
+
258
+ group('Camera JSON Parsing Edge Cases', () {
259
+ test('handles null hostname', () {
260
+ final json = {
261
+ 'hostname': null,
262
+ 'ip_address': '192.168.52.100',
263
+ 'mac_address': 'aa:bb:cc:dd:ee:ff',
264
+ 'last_seen': '2024-01-15T10:30:00Z',
265
+ 'status': 'connected',
266
+ };
267
+
268
+ final camera = Camera.fromJson(json);
269
+ expect(camera.hostname, '');
270
+ });
271
+
272
+ test('handles null ip_address', () {
273
+ final json = {
274
+ 'hostname': 'TestCamera',
275
+ 'ip_address': null,
276
+ 'mac_address': 'aa:bb:cc:dd:ee:ff',
277
+ 'last_seen': '2024-01-15T10:30:00Z',
278
+ 'status': 'connected',
279
+ };
280
+
281
+ final camera = Camera.fromJson(json);
282
+ expect(camera.ipAddress, '');
283
+ });
284
+
285
+ test('handles null mac_address', () {
286
+ final json = {
287
+ 'hostname': 'TestCamera',
288
+ 'ip_address': '192.168.52.100',
289
+ 'mac_address': null,
290
+ 'last_seen': '2024-01-15T10:30:00Z',
291
+ 'status': 'connected',
292
+ };
293
+
294
+ final camera = Camera.fromJson(json);
295
+ expect(camera.macAddress, '');
296
+ });
297
+
298
+ test('handles null last_seen', () {
299
+ final json = {
300
+ 'hostname': 'TestCamera',
301
+ 'ip_address': '192.168.52.100',
302
+ 'mac_address': 'aa:bb:cc:dd:ee:ff',
303
+ 'last_seen': null,
304
+ 'status': 'connected',
305
+ };
306
+
307
+ final camera = Camera.fromJson(json);
308
+ expect(camera.lastSeen, '');
309
+ });
310
+
311
+ test('handles null status with default', () {
312
+ final json = {
313
+ 'hostname': 'TestCamera',
314
+ 'ip_address': '192.168.52.100',
315
+ 'mac_address': 'aa:bb:cc:dd:ee:ff',
316
+ 'last_seen': '2024-01-15T10:30:00Z',
317
+ 'status': null,
318
+ };
319
+
320
+ final camera = Camera.fromJson(json);
321
+ expect(camera.status, 'unknown');
322
+ });
323
+
324
+ test('handles missing fields', () {
325
+ final json = <String, dynamic>{};
326
+
327
+ final camera = Camera.fromJson(json);
328
+ expect(camera.hostname, '');
329
+ expect(camera.ipAddress, '');
330
+ expect(camera.macAddress, '');
331
+ expect(camera.lastSeen, '');
332
+ expect(camera.status, 'unknown');
333
+ });
334
+ });
335
+
336
+ group('Multiple Cameras Scenarios', () {
337
+ test('parses list with mixed connected and disconnected cameras', () {
338
+ final mockResponse = {
339
+ 'cameras': [
340
+ {
341
+ 'hostname': 'Camera1',
342
+ 'ip_address': '192.168.52.101',
343
+ 'mac_address': 'aa:bb:cc:dd:ee:01',
344
+ 'last_seen': '2024-01-15T10:00:00Z',
345
+ 'status': 'connected',
346
+ },
347
+ {
348
+ 'hostname': 'Camera2',
349
+ 'ip_address': '192.168.52.102',
350
+ 'mac_address': 'aa:bb:cc:dd:ee:02',
351
+ 'last_seen': '2024-01-15T09:55:00Z',
352
+ 'status': 'disconnected',
353
+ },
354
+ {
355
+ 'hostname': 'Camera3',
356
+ 'ip_address': '192.168.52.103',
357
+ 'mac_address': 'aa:bb:cc:dd:ee:03',
358
+ 'last_seen': '2024-01-15T10:05:00Z',
359
+ 'status': 'connected',
360
+ },
361
+ ],
362
+ 'count': 3,
363
+ };
364
+
365
+ final camerasList = mockResponse['cameras'] as List;
366
+ final cameras = camerasList
367
+ .map((json) => Camera.fromJson(json as Map<String, dynamic>))
368
+ .toList();
369
+
370
+ expect(cameras.length, 3);
371
+ expect(cameras.where((c) => c.isConnected).length, 2);
372
+ expect(cameras.where((c) => !c.isConnected).length, 1);
373
+ });
374
+
375
+ test('handles cameras with duplicate hostnames', () {
376
+ final mockResponse = {
377
+ 'cameras': [
378
+ {
379
+ 'hostname': 'Microseven',
380
+ 'ip_address': '192.168.52.101',
381
+ 'mac_address': 'aa:bb:cc:dd:ee:01',
382
+ 'last_seen': '2024-01-15T10:00:00Z',
383
+ 'status': 'connected',
384
+ },
385
+ {
386
+ 'hostname': 'Microseven',
387
+ 'ip_address': '192.168.52.102',
388
+ 'mac_address': 'aa:bb:cc:dd:ee:02',
389
+ 'last_seen': '2024-01-15T10:00:00Z',
390
+ 'status': 'connected',
391
+ },
392
+ ],
393
+ 'count': 2,
394
+ };
395
+
396
+ final camerasList = mockResponse['cameras'] as List;
397
+ final cameras = camerasList
398
+ .map((json) => Camera.fromJson(json as Map<String, dynamic>))
399
+ .toList();
400
+
401
+ expect(cameras.length, 2);
402
+ expect(cameras[0].hostname, cameras[1].hostname);
403
+ expect(cameras[0].macAddress, isNot(cameras[1].macAddress));
404
+ expect(cameras[0].ipAddress, isNot(cameras[1].ipAddress));
405
+ });
406
+
407
+ test('handles cameras with special characters in hostname', () {
408
+ final json = {
409
+ 'hostname': 'Camera-1_Test@Site#A',
410
+ 'ip_address': '192.168.52.100',
411
+ 'mac_address': 'aa:bb:cc:dd:ee:ff',
412
+ 'last_seen': '2024-01-15T10:30:00Z',
413
+ 'status': 'connected',
414
+ };
415
+
416
+ final camera = Camera.fromJson(json);
417
+ expect(camera.hostname, 'Camera-1_Test@Site#A');
418
+ expect(camera.displayName, 'Camera-1_Test@Site#A');
419
+ });
420
+ });
421
+
422
+ group('Camera URL Construction', () {
423
+ test('cameraUrl can be used to construct HTTP URL', () {
424
+ final camera = Camera(
425
+ hostname: 'TestCamera',
426
+ ipAddress: '192.168.52.100',
427
+ macAddress: 'aa:bb:cc:dd:ee:ff',
428
+ lastSeen: '2024-01-15T10:30:00Z',
429
+ status: 'connected',
430
+ );
431
+
432
+ final httpUrl = 'http://${camera.cameraUrl}';
433
+ expect(httpUrl, 'http://192.168.52.100');
434
+ });
435
+
436
+ test('cameraUrl handles IPv6 addresses', () {
437
+ final camera = Camera(
438
+ hostname: 'TestCamera',
439
+ ipAddress: 'fe80::1',
440
+ macAddress: 'aa:bb:cc:dd:ee:ff',
441
+ lastSeen: '2024-01-15T10:30:00Z',
442
+ status: 'connected',
443
+ );
444
+
445
+ expect(camera.cameraUrl, 'fe80::1');
446
+ });
447
+
448
+ test('cameraUrl handles IP with port', () {
449
+ final camera = Camera(
450
+ hostname: 'TestCamera',
451
+ ipAddress: '192.168.52.100:8080',
452
+ macAddress: 'aa:bb:cc:dd:ee:ff',
453
+ lastSeen: '2024-01-15T10:30:00Z',
454
+ status: 'connected',
455
+ );
456
+
457
+ expect(camera.cameraUrl, '192.168.52.100:8080');
458
+ });
459
+ });
460
+
461
+ group('Camera Equality and Identity', () {
462
+ test('cameras with same MAC address are identifiable', () {
463
+ final camera1 = Camera(
464
+ hostname: 'Camera1',
465
+ ipAddress: '192.168.52.101',
466
+ macAddress: 'aa:bb:cc:dd:ee:ff',
467
+ lastSeen: '2024-01-15T10:00:00Z',
468
+ status: 'connected',
469
+ );
470
+
471
+ final camera2 = Camera(
472
+ hostname: 'Camera1',
473
+ ipAddress: '192.168.52.102', // Different IP
474
+ macAddress: 'aa:bb:cc:dd:ee:ff', // Same MAC
475
+ lastSeen: '2024-01-15T10:05:00Z',
476
+ status: 'connected',
477
+ );
478
+
479
+ expect(camera1.macAddress, camera2.macAddress);
480
+ });
481
+
482
+ test('cameras with different MAC addresses are distinguishable', () {
483
+ final camera1 = Camera(
484
+ hostname: 'Camera1',
485
+ ipAddress: '192.168.52.101',
486
+ macAddress: 'aa:bb:cc:dd:ee:01',
487
+ lastSeen: '2024-01-15T10:00:00Z',
488
+ status: 'connected',
489
+ );
490
+
491
+ final camera2 = Camera(
492
+ hostname: 'Camera1',
493
+ ipAddress: '192.168.52.101',
494
+ macAddress: 'aa:bb:cc:dd:ee:02',
495
+ lastSeen: '2024-01-15T10:00:00Z',
496
+ status: 'connected',
497
+ );
498
+
499
+ expect(camera1.macAddress, isNot(camera2.macAddress));
500
+ });
501
+ });
502
+
503
+ group('API Response Count Validation', () {
504
+ test('count matches number of cameras in list', () {
505
+ final mockResponse = {
506
+ 'cameras': [
507
+ {
508
+ 'hostname': 'Camera1',
509
+ 'ip_address': '192.168.52.101',
510
+ 'mac_address': 'aa:bb:cc:dd:ee:01',
511
+ 'last_seen': '2024-01-15T10:00:00Z',
512
+ 'status': 'connected',
513
+ },
514
+ {
515
+ 'hostname': 'Camera2',
516
+ 'ip_address': '192.168.52.102',
517
+ 'mac_address': 'aa:bb:cc:dd:ee:02',
518
+ 'last_seen': '2024-01-15T10:00:00Z',
519
+ 'status': 'connected',
520
+ },
521
+ ],
522
+ 'count': 2,
523
+ };
524
+
525
+ final count = mockResponse['count'] as int;
526
+ final cameras = mockResponse['cameras'] as List;
527
+
528
+ expect(count, cameras.length);
529
+ });
530
+
531
+ test('handles empty cameras list with zero count', () {
532
+ final mockResponse = {'cameras': [], 'count': 0};
533
+
534
+ final count = mockResponse['count'] as int;
535
+ final cameras = mockResponse['cameras'] as List;
536
+
537
+ expect(count, 0);
538
+ expect(cameras, isEmpty);
539
+ });
540
+
541
+ test('validates count matches camera list length', () {
542
+ final mockResponse = {
543
+ 'cameras': [
544
+ {
545
+ 'hostname': 'Camera1',
546
+ 'ip_address': '192.168.52.101',
547
+ 'mac_address': 'aa:bb:cc:dd:ee:01',
548
+ 'last_seen': '2024-01-15T10:00:00Z',
549
+ 'status': 'connected',
550
+ },
551
+ {
552
+ 'hostname': 'Camera2',
553
+ 'ip_address': '192.168.52.102',
554
+ 'mac_address': 'aa:bb:cc:dd:ee:02',
555
+ 'last_seen': '2024-01-15T10:05:00Z',
556
+ 'status': 'connected',
557
+ },
558
+ {
559
+ 'hostname': 'Camera3',
560
+ 'ip_address': '192.168.52.103',
561
+ 'mac_address': 'aa:bb:cc:dd:ee:03',
562
+ 'last_seen': '2024-01-15T10:10:00Z',
563
+ 'status': 'disconnected',
564
+ },
565
+ ],
566
+ 'count': 3,
567
+ };
568
+
569
+ final count = mockResponse['count'] as int;
570
+ final camerasList = mockResponse['cameras'] as List;
571
+ final cameras = camerasList
572
+ .map((json) => Camera.fromJson(json as Map<String, dynamic>))
573
+ .toList();
574
+
575
+ expect(count, cameras.length);
576
+ expect(cameras.length, 3);
577
+ });
578
+ });
579
+ });
580
+ }