@qwickapps/server 1.4.0 → 1.5.0

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 (271) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +12 -2
  4. package/dist/index.js.map +1 -1
  5. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -1
  6. package/dist/plugins/bans/bans-plugin.js +12 -3
  7. package/dist/plugins/bans/bans-plugin.js.map +1 -1
  8. package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts +11 -0
  9. package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts.map +1 -0
  10. package/dist/plugins/devices/__tests__/devices-plugin.test.js +410 -0
  11. package/dist/plugins/devices/__tests__/devices-plugin.test.js.map +1 -0
  12. package/dist/plugins/devices/__tests__/token-utils.test.d.ts +7 -0
  13. package/dist/plugins/devices/__tests__/token-utils.test.d.ts.map +1 -0
  14. package/dist/plugins/devices/__tests__/token-utils.test.js +197 -0
  15. package/dist/plugins/devices/__tests__/token-utils.test.js.map +1 -0
  16. package/dist/plugins/devices/adapters/compute-adapter.d.ts +36 -0
  17. package/dist/plugins/devices/adapters/compute-adapter.d.ts.map +1 -0
  18. package/dist/plugins/devices/adapters/compute-adapter.js +100 -0
  19. package/dist/plugins/devices/adapters/compute-adapter.js.map +1 -0
  20. package/dist/plugins/devices/adapters/index.d.ts +12 -0
  21. package/dist/plugins/devices/adapters/index.d.ts.map +1 -0
  22. package/dist/plugins/devices/adapters/index.js +10 -0
  23. package/dist/plugins/devices/adapters/index.js.map +1 -0
  24. package/dist/plugins/devices/adapters/mobile-adapter.d.ts +41 -0
  25. package/dist/plugins/devices/adapters/mobile-adapter.d.ts.map +1 -0
  26. package/dist/plugins/devices/adapters/mobile-adapter.js +131 -0
  27. package/dist/plugins/devices/adapters/mobile-adapter.js.map +1 -0
  28. package/dist/plugins/devices/devices-plugin.d.ts +70 -0
  29. package/dist/plugins/devices/devices-plugin.d.ts.map +1 -0
  30. package/dist/plugins/devices/devices-plugin.js +453 -0
  31. package/dist/plugins/devices/devices-plugin.js.map +1 -0
  32. package/dist/plugins/devices/index.d.ts +18 -0
  33. package/dist/plugins/devices/index.d.ts.map +1 -0
  34. package/dist/plugins/devices/index.js +18 -0
  35. package/dist/plugins/devices/index.js.map +1 -0
  36. package/dist/plugins/devices/stores/index.d.ts +9 -0
  37. package/dist/plugins/devices/stores/index.d.ts.map +1 -0
  38. package/dist/plugins/devices/stores/index.js +9 -0
  39. package/dist/plugins/devices/stores/index.js.map +1 -0
  40. package/dist/plugins/devices/stores/postgres-store.d.ts +26 -0
  41. package/dist/plugins/devices/stores/postgres-store.d.ts.map +1 -0
  42. package/dist/plugins/devices/stores/postgres-store.js +199 -0
  43. package/dist/plugins/devices/stores/postgres-store.js.map +1 -0
  44. package/dist/plugins/devices/token-utils.d.ts +100 -0
  45. package/dist/plugins/devices/token-utils.d.ts.map +1 -0
  46. package/dist/plugins/devices/token-utils.js +162 -0
  47. package/dist/plugins/devices/token-utils.js.map +1 -0
  48. package/dist/plugins/devices/types.d.ts +307 -0
  49. package/dist/plugins/devices/types.d.ts.map +1 -0
  50. package/dist/plugins/devices/types.js +10 -0
  51. package/dist/plugins/devices/types.js.map +1 -0
  52. package/dist/plugins/index.d.ts +14 -2
  53. package/dist/plugins/index.d.ts.map +1 -1
  54. package/dist/plugins/index.js +13 -1
  55. package/dist/plugins/index.js.map +1 -1
  56. package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts +5 -0
  57. package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts.map +1 -0
  58. package/dist/plugins/notifications/__tests__/notifications-manager.test.js +470 -0
  59. package/dist/plugins/notifications/__tests__/notifications-manager.test.js.map +1 -0
  60. package/dist/plugins/notifications/index.d.ts +71 -0
  61. package/dist/plugins/notifications/index.d.ts.map +1 -0
  62. package/dist/plugins/notifications/index.js +72 -0
  63. package/dist/plugins/notifications/index.js.map +1 -0
  64. package/dist/plugins/notifications/notifications-manager.d.ts +182 -0
  65. package/dist/plugins/notifications/notifications-manager.d.ts.map +1 -0
  66. package/dist/plugins/notifications/notifications-manager.js +610 -0
  67. package/dist/plugins/notifications/notifications-manager.js.map +1 -0
  68. package/dist/plugins/notifications/notifications-plugin.d.ts +83 -0
  69. package/dist/plugins/notifications/notifications-plugin.d.ts.map +1 -0
  70. package/dist/plugins/notifications/notifications-plugin.js +337 -0
  71. package/dist/plugins/notifications/notifications-plugin.js.map +1 -0
  72. package/dist/plugins/notifications/types.d.ts +164 -0
  73. package/dist/plugins/notifications/types.d.ts.map +1 -0
  74. package/dist/plugins/notifications/types.js +9 -0
  75. package/dist/plugins/notifications/types.js.map +1 -0
  76. package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts +12 -0
  77. package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts.map +1 -0
  78. package/dist/plugins/parental/__tests__/parental-plugin.test.js +349 -0
  79. package/dist/plugins/parental/__tests__/parental-plugin.test.js.map +1 -0
  80. package/dist/plugins/parental/adapters/index.d.ts +8 -0
  81. package/dist/plugins/parental/adapters/index.d.ts.map +1 -0
  82. package/dist/plugins/parental/adapters/index.js +7 -0
  83. package/dist/plugins/parental/adapters/index.js.map +1 -0
  84. package/dist/plugins/parental/adapters/kids-adapter.d.ts +24 -0
  85. package/dist/plugins/parental/adapters/kids-adapter.d.ts.map +1 -0
  86. package/dist/plugins/parental/adapters/kids-adapter.js +174 -0
  87. package/dist/plugins/parental/adapters/kids-adapter.js.map +1 -0
  88. package/dist/plugins/parental/index.d.ts +14 -0
  89. package/dist/plugins/parental/index.d.ts.map +1 -0
  90. package/dist/plugins/parental/index.js +15 -0
  91. package/dist/plugins/parental/index.js.map +1 -0
  92. package/dist/plugins/parental/parental-plugin.d.ts +88 -0
  93. package/dist/plugins/parental/parental-plugin.d.ts.map +1 -0
  94. package/dist/plugins/parental/parental-plugin.js +666 -0
  95. package/dist/plugins/parental/parental-plugin.js.map +1 -0
  96. package/dist/plugins/parental/stores/index.d.ts +7 -0
  97. package/dist/plugins/parental/stores/index.d.ts.map +1 -0
  98. package/dist/plugins/parental/stores/index.js +7 -0
  99. package/dist/plugins/parental/stores/index.js.map +1 -0
  100. package/dist/plugins/parental/stores/postgres-store.d.ts +10 -0
  101. package/dist/plugins/parental/stores/postgres-store.d.ts.map +1 -0
  102. package/dist/plugins/parental/stores/postgres-store.js +209 -0
  103. package/dist/plugins/parental/stores/postgres-store.js.map +1 -0
  104. package/dist/plugins/parental/types.d.ts +154 -0
  105. package/dist/plugins/parental/types.d.ts.map +1 -0
  106. package/dist/plugins/parental/types.js +10 -0
  107. package/dist/plugins/parental/types.js.map +1 -0
  108. package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts +11 -0
  109. package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts.map +1 -0
  110. package/dist/plugins/profiles/__tests__/profiles-plugin.test.js +243 -0
  111. package/dist/plugins/profiles/__tests__/profiles-plugin.test.js.map +1 -0
  112. package/dist/plugins/profiles/index.d.ts +12 -0
  113. package/dist/plugins/profiles/index.d.ts.map +1 -0
  114. package/dist/plugins/profiles/index.js +13 -0
  115. package/dist/plugins/profiles/index.js.map +1 -0
  116. package/dist/plugins/profiles/profiles-plugin.d.ts +71 -0
  117. package/dist/plugins/profiles/profiles-plugin.d.ts.map +1 -0
  118. package/dist/plugins/profiles/profiles-plugin.js +481 -0
  119. package/dist/plugins/profiles/profiles-plugin.js.map +1 -0
  120. package/dist/plugins/profiles/stores/index.d.ts +9 -0
  121. package/dist/plugins/profiles/stores/index.d.ts.map +1 -0
  122. package/dist/plugins/profiles/stores/index.js +9 -0
  123. package/dist/plugins/profiles/stores/index.js.map +1 -0
  124. package/dist/plugins/profiles/stores/postgres-store.d.ts +18 -0
  125. package/dist/plugins/profiles/stores/postgres-store.d.ts.map +1 -0
  126. package/dist/plugins/profiles/stores/postgres-store.js +310 -0
  127. package/dist/plugins/profiles/stores/postgres-store.js.map +1 -0
  128. package/dist/plugins/profiles/types.d.ts +289 -0
  129. package/dist/plugins/profiles/types.d.ts.map +1 -0
  130. package/dist/plugins/profiles/types.js +10 -0
  131. package/dist/plugins/profiles/types.js.map +1 -0
  132. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts +11 -0
  133. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts.map +1 -0
  134. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js +305 -0
  135. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js.map +1 -0
  136. package/dist/plugins/subscriptions/index.d.ts +12 -0
  137. package/dist/plugins/subscriptions/index.d.ts.map +1 -0
  138. package/dist/plugins/subscriptions/index.js +13 -0
  139. package/dist/plugins/subscriptions/index.js.map +1 -0
  140. package/dist/plugins/subscriptions/stores/index.d.ts +9 -0
  141. package/dist/plugins/subscriptions/stores/index.d.ts.map +1 -0
  142. package/dist/plugins/subscriptions/stores/index.js +9 -0
  143. package/dist/plugins/subscriptions/stores/index.js.map +1 -0
  144. package/dist/plugins/subscriptions/stores/postgres-store.d.ts +14 -0
  145. package/dist/plugins/subscriptions/stores/postgres-store.d.ts.map +1 -0
  146. package/dist/plugins/subscriptions/stores/postgres-store.js +359 -0
  147. package/dist/plugins/subscriptions/stores/postgres-store.js.map +1 -0
  148. package/dist/plugins/subscriptions/subscriptions-plugin.d.ts +82 -0
  149. package/dist/plugins/subscriptions/subscriptions-plugin.d.ts.map +1 -0
  150. package/dist/plugins/subscriptions/subscriptions-plugin.js +449 -0
  151. package/dist/plugins/subscriptions/subscriptions-plugin.js.map +1 -0
  152. package/dist/plugins/subscriptions/types.d.ts +308 -0
  153. package/dist/plugins/subscriptions/types.d.ts.map +1 -0
  154. package/dist/plugins/subscriptions/types.js +10 -0
  155. package/dist/plugins/subscriptions/types.js.map +1 -0
  156. package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts +11 -0
  157. package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts.map +1 -0
  158. package/dist/plugins/usage/__tests__/usage-plugin.test.js +218 -0
  159. package/dist/plugins/usage/__tests__/usage-plugin.test.js.map +1 -0
  160. package/dist/plugins/usage/index.d.ts +12 -0
  161. package/dist/plugins/usage/index.d.ts.map +1 -0
  162. package/dist/plugins/usage/index.js +13 -0
  163. package/dist/plugins/usage/index.js.map +1 -0
  164. package/dist/plugins/usage/stores/index.d.ts +9 -0
  165. package/dist/plugins/usage/stores/index.d.ts.map +1 -0
  166. package/dist/plugins/usage/stores/index.js +9 -0
  167. package/dist/plugins/usage/stores/index.js.map +1 -0
  168. package/dist/plugins/usage/stores/postgres-store.d.ts +14 -0
  169. package/dist/plugins/usage/stores/postgres-store.d.ts.map +1 -0
  170. package/dist/plugins/usage/stores/postgres-store.js +146 -0
  171. package/dist/plugins/usage/stores/postgres-store.js.map +1 -0
  172. package/dist/plugins/usage/types.d.ts +195 -0
  173. package/dist/plugins/usage/types.d.ts.map +1 -0
  174. package/dist/plugins/usage/types.js +10 -0
  175. package/dist/plugins/usage/types.js.map +1 -0
  176. package/dist/plugins/usage/usage-plugin.d.ts +51 -0
  177. package/dist/plugins/usage/usage-plugin.d.ts.map +1 -0
  178. package/dist/plugins/usage/usage-plugin.js +412 -0
  179. package/dist/plugins/usage/usage-plugin.js.map +1 -0
  180. package/dist/plugins/users/__tests__/postgres-store.test.d.ts +10 -0
  181. package/dist/plugins/users/__tests__/postgres-store.test.d.ts.map +1 -0
  182. package/dist/plugins/users/__tests__/postgres-store.test.js +229 -0
  183. package/dist/plugins/users/__tests__/postgres-store.test.js.map +1 -0
  184. package/dist/plugins/users/__tests__/users-plugin.test.js +3 -0
  185. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -1
  186. package/dist/plugins/users/index.d.ts +2 -2
  187. package/dist/plugins/users/index.d.ts.map +1 -1
  188. package/dist/plugins/users/index.js +1 -1
  189. package/dist/plugins/users/index.js.map +1 -1
  190. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -1
  191. package/dist/plugins/users/stores/postgres-store.js +76 -0
  192. package/dist/plugins/users/stores/postgres-store.js.map +1 -1
  193. package/dist/plugins/users/types.d.ts +74 -6
  194. package/dist/plugins/users/types.d.ts.map +1 -1
  195. package/dist/plugins/users/users-plugin.d.ts +15 -1
  196. package/dist/plugins/users/users-plugin.d.ts.map +1 -1
  197. package/dist/plugins/users/users-plugin.js +29 -0
  198. package/dist/plugins/users/users-plugin.js.map +1 -1
  199. package/dist-ui/assets/index-CynOqPkb.js +469 -0
  200. package/dist-ui/assets/index-CynOqPkb.js.map +1 -0
  201. package/dist-ui/index.html +1 -1
  202. package/dist-ui-lib/api/controlPanelApi.d.ts +46 -0
  203. package/dist-ui-lib/components/StatCard.d.ts +16 -0
  204. package/dist-ui-lib/dashboard/widgets/NotificationsStatsWidget.d.ts +12 -0
  205. package/dist-ui-lib/dashboard/widgets/index.d.ts +1 -0
  206. package/dist-ui-lib/index.js +1822 -1611
  207. package/dist-ui-lib/index.js.map +1 -1
  208. package/dist-ui-lib/pages/NotificationsPage.d.ts +9 -0
  209. package/dist-ui-lib/utils/formatters.d.ts +19 -0
  210. package/package.json +1 -1
  211. package/src/index.ts +178 -0
  212. package/src/plugins/bans/bans-plugin.ts +15 -3
  213. package/src/plugins/devices/__tests__/devices-plugin.test.ts +551 -0
  214. package/src/plugins/devices/__tests__/token-utils.test.ts +264 -0
  215. package/src/plugins/devices/adapters/compute-adapter.ts +139 -0
  216. package/src/plugins/devices/adapters/index.ts +13 -0
  217. package/src/plugins/devices/adapters/mobile-adapter.ts +179 -0
  218. package/src/plugins/devices/devices-plugin.ts +538 -0
  219. package/src/plugins/devices/index.ts +69 -0
  220. package/src/plugins/devices/stores/index.ts +9 -0
  221. package/src/plugins/devices/stores/postgres-store.ts +304 -0
  222. package/src/plugins/devices/token-utils.ts +213 -0
  223. package/src/plugins/devices/types.ts +351 -0
  224. package/src/plugins/index.ts +218 -0
  225. package/src/plugins/notifications/__tests__/notifications-manager.test.ts +637 -0
  226. package/src/plugins/notifications/index.ts +91 -0
  227. package/src/plugins/notifications/notifications-manager.ts +773 -0
  228. package/src/plugins/notifications/notifications-plugin.ts +398 -0
  229. package/src/plugins/notifications/types.ts +207 -0
  230. package/src/plugins/parental/__tests__/parental-plugin.test.ts +465 -0
  231. package/src/plugins/parental/adapters/index.ts +8 -0
  232. package/src/plugins/parental/adapters/kids-adapter.ts +206 -0
  233. package/src/plugins/parental/index.ts +55 -0
  234. package/src/plugins/parental/parental-plugin.ts +759 -0
  235. package/src/plugins/parental/stores/index.ts +7 -0
  236. package/src/plugins/parental/stores/postgres-store.ts +304 -0
  237. package/src/plugins/parental/types.ts +180 -0
  238. package/src/plugins/profiles/__tests__/profiles-plugin.test.ts +321 -0
  239. package/src/plugins/profiles/index.ts +49 -0
  240. package/src/plugins/profiles/profiles-plugin.ts +546 -0
  241. package/src/plugins/profiles/stores/index.ts +9 -0
  242. package/src/plugins/profiles/stores/postgres-store.ts +439 -0
  243. package/src/plugins/profiles/types.ts +338 -0
  244. package/src/plugins/subscriptions/__tests__/subscriptions-plugin.test.ts +404 -0
  245. package/src/plugins/subscriptions/index.ts +51 -0
  246. package/src/plugins/subscriptions/stores/index.ts +9 -0
  247. package/src/plugins/subscriptions/stores/postgres-store.ts +482 -0
  248. package/src/plugins/subscriptions/subscriptions-plugin.ts +530 -0
  249. package/src/plugins/subscriptions/types.ts +355 -0
  250. package/src/plugins/usage/__tests__/usage-plugin.test.ts +288 -0
  251. package/src/plugins/usage/index.ts +39 -0
  252. package/src/plugins/usage/stores/index.ts +9 -0
  253. package/src/plugins/usage/stores/postgres-store.ts +213 -0
  254. package/src/plugins/usage/types.ts +222 -0
  255. package/src/plugins/usage/usage-plugin.ts +484 -0
  256. package/src/plugins/users/__tests__/postgres-store.test.ts +326 -0
  257. package/src/plugins/users/__tests__/users-plugin.test.ts +3 -0
  258. package/src/plugins/users/index.ts +6 -0
  259. package/src/plugins/users/stores/postgres-store.ts +104 -0
  260. package/src/plugins/users/types.ts +82 -6
  261. package/src/plugins/users/users-plugin.ts +37 -0
  262. package/ui/src/App.tsx +5 -1
  263. package/ui/src/api/controlPanelApi.ts +103 -6
  264. package/ui/src/components/StatCard.tsx +58 -0
  265. package/ui/src/dashboard/builtInWidgets.tsx +3 -1
  266. package/ui/src/dashboard/widgets/NotificationsStatsWidget.tsx +167 -0
  267. package/ui/src/dashboard/widgets/index.ts +1 -0
  268. package/ui/src/pages/NotificationsPage.tsx +417 -0
  269. package/ui/src/utils/formatters.ts +33 -0
  270. package/dist-ui/assets/index-D7DoZ9rL.js +0 -478
  271. package/dist-ui/assets/index-D7DoZ9rL.js.map +0 -1
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Devices Plugin Tests
3
+ *
4
+ * Unit tests for the devices plugin including:
5
+ * - Plugin lifecycle (start, stop)
6
+ * - Device registration
7
+ * - Token verification
8
+ * - Helper functions
9
+ */
10
+
11
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
12
+ import {
13
+ createDevicesPlugin,
14
+ getDeviceStore,
15
+ getDeviceAdapter,
16
+ registerDevice,
17
+ verifyDeviceToken,
18
+ getDeviceById,
19
+ updateDevice,
20
+ deleteDevice,
21
+ regenerateToken,
22
+ listUserDevices,
23
+ deactivateDevice,
24
+ activateDevice,
25
+ } from '../devices-plugin.js';
26
+ import type {
27
+ DeviceStore,
28
+ DeviceAdapter,
29
+ Device,
30
+ CreateDeviceInput,
31
+ } from '../types.js';
32
+ import type { PluginRegistry } from '../../../core/plugin-registry.js';
33
+
34
+ describe('Devices Plugin', () => {
35
+ // Mock device data
36
+ const mockDevice: Device = {
37
+ id: 'device-id-123',
38
+ org_id: 'org-123',
39
+ user_id: 'user-123',
40
+ adapter_type: 'mobile',
41
+ name: 'Test Phone',
42
+ token_prefix: 'mob_1234',
43
+ token_expires_at: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
44
+ last_seen_at: new Date(),
45
+ last_ip: '192.168.1.1',
46
+ is_active: true,
47
+ metadata: { device_model: 'iPhone 15', os_name: 'iOS', os_version: '17.0' },
48
+ created_at: new Date(),
49
+ updated_at: new Date(),
50
+ };
51
+
52
+ // Mock store factory
53
+ const createMockStore = (): DeviceStore => ({
54
+ name: 'mock',
55
+ initialize: vi.fn().mockResolvedValue(undefined),
56
+ getById: vi.fn().mockResolvedValue(mockDevice),
57
+ getByTokenHash: vi.fn().mockResolvedValue(mockDevice),
58
+ create: vi.fn().mockResolvedValue(mockDevice),
59
+ update: vi.fn().mockResolvedValue(mockDevice),
60
+ delete: vi.fn().mockResolvedValue(true),
61
+ search: vi.fn().mockResolvedValue({
62
+ devices: [mockDevice],
63
+ total: 1,
64
+ page: 1,
65
+ limit: 20,
66
+ totalPages: 1,
67
+ }),
68
+ updateLastSeen: vi.fn().mockResolvedValue(undefined),
69
+ updateToken: vi.fn().mockResolvedValue(true),
70
+ cleanupExpired: vi.fn().mockResolvedValue(5),
71
+ shutdown: vi.fn().mockResolvedValue(undefined),
72
+ });
73
+
74
+ // Mock adapter factory
75
+ const createMockAdapter = (): DeviceAdapter => ({
76
+ name: 'mobile',
77
+ tokenPrefix: 'mob',
78
+ validateDeviceInput: vi.fn().mockReturnValue({ valid: true }),
79
+ transformForStorage: vi.fn().mockImplementation((input) => input.metadata || {}),
80
+ transformFromStorage: vi.fn().mockImplementation((row) => row),
81
+ onDeviceCreated: vi.fn().mockResolvedValue(undefined),
82
+ onDeviceDeleted: vi.fn().mockResolvedValue(undefined),
83
+ onDeviceVerified: vi.fn().mockResolvedValue(undefined),
84
+ });
85
+
86
+ // Mock registry factory
87
+ const createMockRegistry = (): PluginRegistry =>
88
+ ({
89
+ hasPlugin: vi.fn().mockReturnValue(false),
90
+ getPlugin: vi.fn().mockReturnValue(null),
91
+ listPlugins: vi.fn().mockReturnValue([]),
92
+ addRoute: vi.fn(),
93
+ addMenuItem: vi.fn(),
94
+ addPage: vi.fn(),
95
+ addWidget: vi.fn(),
96
+ getRoutes: vi.fn().mockReturnValue([]),
97
+ getMenuItems: vi.fn().mockReturnValue([]),
98
+ getPages: vi.fn().mockReturnValue([]),
99
+ getWidgets: vi.fn().mockReturnValue([]),
100
+ getConfig: vi.fn().mockReturnValue({}),
101
+ setConfig: vi.fn().mockResolvedValue(undefined),
102
+ subscribe: vi.fn().mockReturnValue(() => {}),
103
+ emit: vi.fn(),
104
+ registerHealthCheck: vi.fn(),
105
+ getApp: vi.fn().mockReturnValue({} as any),
106
+ getRouter: vi.fn().mockReturnValue({} as any),
107
+ getLogger: vi.fn().mockReturnValue({
108
+ debug: vi.fn(),
109
+ info: vi.fn(),
110
+ warn: vi.fn(),
111
+ error: vi.fn(),
112
+ }),
113
+ }) as unknown as PluginRegistry;
114
+
115
+ let mockStore: DeviceStore;
116
+ let mockAdapter: DeviceAdapter;
117
+ let mockRegistry: PluginRegistry;
118
+
119
+ beforeEach(() => {
120
+ vi.clearAllMocks();
121
+ mockStore = createMockStore();
122
+ mockAdapter = createMockAdapter();
123
+ mockRegistry = createMockRegistry();
124
+ });
125
+
126
+ afterEach(async () => {
127
+ // Clean up by stopping the plugin if it was started
128
+ const store = getDeviceStore();
129
+ if (store) {
130
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
131
+ await plugin.onStop?.();
132
+ }
133
+ });
134
+
135
+ describe('createDevicesPlugin', () => {
136
+ it('should create a plugin with correct id', () => {
137
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
138
+ expect(plugin.id).toBe('devices');
139
+ });
140
+
141
+ it('should create a plugin with correct name', () => {
142
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
143
+ expect(plugin.name).toBe('Devices');
144
+ });
145
+
146
+ it('should create a plugin with version', () => {
147
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
148
+ expect(plugin.version).toBe('1.0.0');
149
+ });
150
+ });
151
+
152
+ describe('onStart', () => {
153
+ it('should initialize store on start', async () => {
154
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
155
+ await plugin.onStart({}, mockRegistry);
156
+
157
+ expect(mockStore.initialize).toHaveBeenCalled();
158
+ });
159
+
160
+ it('should register health check', async () => {
161
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
162
+ await plugin.onStart({}, mockRegistry);
163
+
164
+ expect(mockRegistry.registerHealthCheck).toHaveBeenCalledWith(
165
+ expect.objectContaining({
166
+ name: 'devices-store',
167
+ type: 'custom',
168
+ })
169
+ );
170
+ });
171
+
172
+ it('should register CRUD routes by default', async () => {
173
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
174
+ await plugin.onStart({}, mockRegistry);
175
+
176
+ // GET /devices, GET /devices/:id, POST /devices, PUT /devices/:id,
177
+ // DELETE /devices/:id, POST /devices/:id/regenerate-token, POST /devices/verify
178
+ expect(mockRegistry.addRoute).toHaveBeenCalledTimes(7);
179
+ });
180
+
181
+ it('should use custom API prefix when provided', async () => {
182
+ const plugin = createDevicesPlugin({
183
+ store: mockStore,
184
+ adapter: mockAdapter,
185
+ api: { prefix: '/api/devices' },
186
+ });
187
+ await plugin.onStart({}, mockRegistry);
188
+
189
+ const calls = (mockRegistry.addRoute as any).mock.calls;
190
+ expect(calls[0][0].path).toBe('/api/devices');
191
+ });
192
+
193
+ it('should skip CRUD routes when disabled', async () => {
194
+ const plugin = createDevicesPlugin({
195
+ store: mockStore,
196
+ adapter: mockAdapter,
197
+ api: { crud: false },
198
+ });
199
+ await plugin.onStart({}, mockRegistry);
200
+
201
+ // Only verification endpoint
202
+ expect(mockRegistry.addRoute).toHaveBeenCalledTimes(1);
203
+ });
204
+
205
+ it('should skip verify endpoint when disabled', async () => {
206
+ const plugin = createDevicesPlugin({
207
+ store: mockStore,
208
+ adapter: mockAdapter,
209
+ api: { verify: false },
210
+ });
211
+ await plugin.onStart({}, mockRegistry);
212
+
213
+ // 6 CRUD routes, no verify
214
+ expect(mockRegistry.addRoute).toHaveBeenCalledTimes(6);
215
+ });
216
+ });
217
+
218
+ describe('onStop', () => {
219
+ it('should shutdown store', async () => {
220
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
221
+ await plugin.onStart({}, mockRegistry);
222
+ await plugin.onStop?.();
223
+
224
+ expect(mockStore.shutdown).toHaveBeenCalled();
225
+ });
226
+
227
+ it('should clear store reference', async () => {
228
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
229
+ await plugin.onStart({}, mockRegistry);
230
+ await plugin.onStop?.();
231
+
232
+ expect(getDeviceStore()).toBeNull();
233
+ });
234
+
235
+ it('should clear adapter reference', async () => {
236
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
237
+ await plugin.onStart({}, mockRegistry);
238
+ await plugin.onStop?.();
239
+
240
+ expect(getDeviceAdapter()).toBeNull();
241
+ });
242
+ });
243
+
244
+ describe('helper functions', () => {
245
+ describe('getDeviceStore', () => {
246
+ it('should return null when plugin not started', () => {
247
+ expect(getDeviceStore()).toBeNull();
248
+ });
249
+
250
+ it('should return store when plugin started', async () => {
251
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
252
+ await plugin.onStart({}, mockRegistry);
253
+
254
+ expect(getDeviceStore()).toBe(mockStore);
255
+ });
256
+ });
257
+
258
+ describe('getDeviceAdapter', () => {
259
+ it('should return null when plugin not started', () => {
260
+ expect(getDeviceAdapter()).toBeNull();
261
+ });
262
+
263
+ it('should return adapter when plugin started', async () => {
264
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
265
+ await plugin.onStart({}, mockRegistry);
266
+
267
+ expect(getDeviceAdapter()).toBe(mockAdapter);
268
+ });
269
+ });
270
+
271
+ describe('registerDevice', () => {
272
+ it('should throw when plugin not initialized', async () => {
273
+ await expect(
274
+ registerDevice({ name: 'Test Device' })
275
+ ).rejects.toThrow('Devices plugin not initialized');
276
+ });
277
+
278
+ it('should create device with generated token', async () => {
279
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
280
+ await plugin.onStart({}, mockRegistry);
281
+
282
+ const result = await registerDevice({
283
+ name: 'Test Device',
284
+ user_id: 'user-123',
285
+ });
286
+
287
+ expect(result.id).toBe(mockDevice.id);
288
+ expect(result.token).toBeDefined();
289
+ expect(mockStore.create).toHaveBeenCalled();
290
+ });
291
+
292
+ it('should throw on validation failure', async () => {
293
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
294
+ await plugin.onStart({}, mockRegistry);
295
+
296
+ (mockAdapter.validateDeviceInput as any).mockReturnValue({
297
+ valid: false,
298
+ errors: ['Name is required'],
299
+ });
300
+
301
+ await expect(
302
+ registerDevice({ name: '' })
303
+ ).rejects.toThrow('Validation failed: Name is required');
304
+ });
305
+
306
+ it('should call adapter onDeviceCreated hook', async () => {
307
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
308
+ await plugin.onStart({}, mockRegistry);
309
+
310
+ await registerDevice({ name: 'Test Device' });
311
+
312
+ expect(mockAdapter.onDeviceCreated).toHaveBeenCalledWith(mockDevice);
313
+ });
314
+ });
315
+
316
+ describe('verifyDeviceToken', () => {
317
+ it('should return error when plugin not initialized', async () => {
318
+ const result = await verifyDeviceToken('mob_testtoken');
319
+
320
+ expect(result.valid).toBe(false);
321
+ expect(result.error).toBe('Devices plugin not initialized');
322
+ });
323
+
324
+ it('should return error for invalid token format', async () => {
325
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
326
+ await plugin.onStart({}, mockRegistry);
327
+
328
+ const result = await verifyDeviceToken('invalid_format');
329
+
330
+ expect(result.valid).toBe(false);
331
+ expect(result.error).toBe('Invalid token format');
332
+ });
333
+
334
+ it('should return error when token not found', async () => {
335
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
336
+ await plugin.onStart({}, mockRegistry);
337
+
338
+ (mockStore.getByTokenHash as any).mockResolvedValue(null);
339
+
340
+ // Valid format token (43 chars after prefix)
341
+ const validToken = 'mob_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk123456';
342
+ const result = await verifyDeviceToken(validToken);
343
+
344
+ expect(result.valid).toBe(false);
345
+ expect(result.error).toBe('Token not found or expired');
346
+ });
347
+
348
+ it('should return error for expired token', async () => {
349
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
350
+ await plugin.onStart({}, mockRegistry);
351
+
352
+ const expiredDevice = {
353
+ ...mockDevice,
354
+ token_expires_at: new Date(Date.now() - 1000), // expired
355
+ };
356
+ (mockStore.getByTokenHash as any).mockResolvedValue(expiredDevice);
357
+
358
+ const validToken = 'mob_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk123456';
359
+ const result = await verifyDeviceToken(validToken);
360
+
361
+ expect(result.valid).toBe(false);
362
+ expect(result.error).toBe('Token has expired');
363
+ });
364
+
365
+ it('should return error for inactive device', async () => {
366
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
367
+ await plugin.onStart({}, mockRegistry);
368
+
369
+ const inactiveDevice = { ...mockDevice, is_active: false };
370
+ (mockStore.getByTokenHash as any).mockResolvedValue(inactiveDevice);
371
+
372
+ const validToken = 'mob_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk123456';
373
+ const result = await verifyDeviceToken(validToken);
374
+
375
+ expect(result.valid).toBe(false);
376
+ expect(result.error).toBe('Device is not active');
377
+ });
378
+
379
+ it('should return valid device and update last seen', async () => {
380
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
381
+ await plugin.onStart({}, mockRegistry);
382
+
383
+ const validToken = 'mob_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk123456';
384
+ const result = await verifyDeviceToken(validToken, '10.0.0.1');
385
+
386
+ expect(result.valid).toBe(true);
387
+ expect(result.device).toEqual(mockDevice);
388
+ expect(mockStore.updateLastSeen).toHaveBeenCalledWith(mockDevice.id, '10.0.0.1');
389
+ });
390
+
391
+ it('should call adapter onDeviceVerified hook', async () => {
392
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
393
+ await plugin.onStart({}, mockRegistry);
394
+
395
+ const validToken = 'mob_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk123456';
396
+ await verifyDeviceToken(validToken, '10.0.0.1');
397
+
398
+ expect(mockAdapter.onDeviceVerified).toHaveBeenCalledWith(mockDevice, '10.0.0.1');
399
+ });
400
+ });
401
+
402
+ describe('getDeviceById', () => {
403
+ it('should throw when plugin not initialized', async () => {
404
+ await expect(getDeviceById('device-id')).rejects.toThrow(
405
+ 'Devices plugin not initialized'
406
+ );
407
+ });
408
+
409
+ it('should return device when found', async () => {
410
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
411
+ await plugin.onStart({}, mockRegistry);
412
+
413
+ const result = await getDeviceById(mockDevice.id);
414
+ expect(result).toEqual(mockDevice);
415
+ });
416
+
417
+ it('should return null when not found', async () => {
418
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
419
+ await plugin.onStart({}, mockRegistry);
420
+
421
+ (mockStore.getById as any).mockResolvedValue(null);
422
+
423
+ const result = await getDeviceById('non-existent');
424
+ expect(result).toBeNull();
425
+ });
426
+ });
427
+
428
+ describe('updateDevice', () => {
429
+ it('should throw when plugin not initialized', async () => {
430
+ await expect(
431
+ updateDevice('device-id', { name: 'New Name' })
432
+ ).rejects.toThrow('Devices plugin not initialized');
433
+ });
434
+
435
+ it('should update device', async () => {
436
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
437
+ await plugin.onStart({}, mockRegistry);
438
+
439
+ const updated = { ...mockDevice, name: 'Updated Name' };
440
+ (mockStore.update as any).mockResolvedValue(updated);
441
+
442
+ const result = await updateDevice(mockDevice.id, { name: 'Updated Name' });
443
+
444
+ expect(result?.name).toBe('Updated Name');
445
+ expect(mockStore.update).toHaveBeenCalledWith(mockDevice.id, { name: 'Updated Name' });
446
+ });
447
+ });
448
+
449
+ describe('deleteDevice', () => {
450
+ it('should throw when plugin not initialized', async () => {
451
+ await expect(deleteDevice('device-id')).rejects.toThrow(
452
+ 'Devices plugin not initialized'
453
+ );
454
+ });
455
+
456
+ it('should delete device and call adapter hook', async () => {
457
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
458
+ await plugin.onStart({}, mockRegistry);
459
+
460
+ const result = await deleteDevice(mockDevice.id);
461
+
462
+ expect(result).toBe(true);
463
+ expect(mockStore.delete).toHaveBeenCalledWith(mockDevice.id);
464
+ expect(mockAdapter.onDeviceDeleted).toHaveBeenCalledWith(mockDevice);
465
+ });
466
+
467
+ it('should return false when device not found', async () => {
468
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
469
+ await plugin.onStart({}, mockRegistry);
470
+
471
+ (mockStore.getById as any).mockResolvedValue(null);
472
+
473
+ const result = await deleteDevice('non-existent');
474
+
475
+ expect(result).toBe(false);
476
+ });
477
+ });
478
+
479
+ describe('regenerateToken', () => {
480
+ it('should throw when plugin not initialized', async () => {
481
+ await expect(regenerateToken('device-id')).rejects.toThrow(
482
+ 'Devices plugin not initialized'
483
+ );
484
+ });
485
+
486
+ it('should return null when device not found', async () => {
487
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
488
+ await plugin.onStart({}, mockRegistry);
489
+
490
+ (mockStore.getById as any).mockResolvedValue(null);
491
+
492
+ const result = await regenerateToken('non-existent');
493
+
494
+ expect(result).toBeNull();
495
+ });
496
+
497
+ it('should regenerate token and return new token', async () => {
498
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
499
+ await plugin.onStart({}, mockRegistry);
500
+
501
+ const result = await regenerateToken(mockDevice.id, 30);
502
+
503
+ expect(result).not.toBeNull();
504
+ expect(result?.token).toMatch(/^mob_/);
505
+ expect(result?.expiresAt).toBeInstanceOf(Date);
506
+ expect(mockStore.updateToken).toHaveBeenCalled();
507
+ });
508
+ });
509
+
510
+ describe('listUserDevices', () => {
511
+ it('should throw when plugin not initialized', async () => {
512
+ await expect(listUserDevices('user-id')).rejects.toThrow(
513
+ 'Devices plugin not initialized'
514
+ );
515
+ });
516
+
517
+ it('should return user devices', async () => {
518
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
519
+ await plugin.onStart({}, mockRegistry);
520
+
521
+ const result = await listUserDevices('user-123');
522
+
523
+ expect(result).toHaveLength(1);
524
+ expect(result[0]).toEqual(mockDevice);
525
+ expect(mockStore.search).toHaveBeenCalledWith({ user_id: 'user-123', limit: 100 });
526
+ });
527
+ });
528
+
529
+ describe('deactivateDevice / activateDevice', () => {
530
+ it('should deactivate device', async () => {
531
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
532
+ await plugin.onStart({}, mockRegistry);
533
+
534
+ const result = await deactivateDevice(mockDevice.id);
535
+
536
+ expect(result).toBe(true);
537
+ expect(mockStore.update).toHaveBeenCalledWith(mockDevice.id, { is_active: false });
538
+ });
539
+
540
+ it('should activate device', async () => {
541
+ const plugin = createDevicesPlugin({ store: mockStore, adapter: mockAdapter });
542
+ await plugin.onStart({}, mockRegistry);
543
+
544
+ const result = await activateDevice(mockDevice.id);
545
+
546
+ expect(result).toBe(true);
547
+ expect(mockStore.update).toHaveBeenCalledWith(mockDevice.id, { is_active: true });
548
+ });
549
+ });
550
+ });
551
+ });