@edge-base/server 0.1.1

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 (309) hide show
  1. package/admin-build/.gitkeep +0 -0
  2. package/admin-build/_app/env.js +1 -0
  3. package/admin-build/_app/immutable/assets/0.Bm6cF078.css +1 -0
  4. package/admin-build/_app/immutable/assets/1.BfW3pUNa.css +1 -0
  5. package/admin-build/_app/immutable/assets/11.CVmQOewb.css +1 -0
  6. package/admin-build/_app/immutable/assets/12.B1EhbRZT.css +1 -0
  7. package/admin-build/_app/immutable/assets/13.BvwYeuwE.css +1 -0
  8. package/admin-build/_app/immutable/assets/14.CdVfcO0R.css +1 -0
  9. package/admin-build/_app/immutable/assets/15.2yeZ66b-.css +1 -0
  10. package/admin-build/_app/immutable/assets/17.BVg0JEVu.css +1 -0
  11. package/admin-build/_app/immutable/assets/18.Rwnl3x_i.css +1 -0
  12. package/admin-build/_app/immutable/assets/20.DsPWA9AV.css +1 -0
  13. package/admin-build/_app/immutable/assets/21.Dz2RJ56c.css +1 -0
  14. package/admin-build/_app/immutable/assets/22.DwNLk5Ai.css +1 -0
  15. package/admin-build/_app/immutable/assets/23.CFpu0gOO.css +1 -0
  16. package/admin-build/_app/immutable/assets/24.Cy5LBeoJ.css +1 -0
  17. package/admin-build/_app/immutable/assets/25.pUyLVf-h.css +1 -0
  18. package/admin-build/_app/immutable/assets/26.DBcGrlXa.css +1 -0
  19. package/admin-build/_app/immutable/assets/27.BswYyAJD.css +1 -0
  20. package/admin-build/_app/immutable/assets/28.B4ueB1Kf.css +1 -0
  21. package/admin-build/_app/immutable/assets/29.B-qU6PdF.css +1 -0
  22. package/admin-build/_app/immutable/assets/3.Dg81Pgmd.css +1 -0
  23. package/admin-build/_app/immutable/assets/30.CsdWum94.css +1 -0
  24. package/admin-build/_app/immutable/assets/31.U6OwIp50.css +1 -0
  25. package/admin-build/_app/immutable/assets/4.CyawCCux.css +1 -0
  26. package/admin-build/_app/immutable/assets/5.C0YO2HTk.css +1 -0
  27. package/admin-build/_app/immutable/assets/8.Br5jd6kD.css +1 -0
  28. package/admin-build/_app/immutable/assets/Badge.EMYLHBxE.css +1 -0
  29. package/admin-build/_app/immutable/assets/Button.DpzMRTjK.css +1 -0
  30. package/admin-build/_app/immutable/assets/ConfirmDialog.DAnaWRRk.css +1 -0
  31. package/admin-build/_app/immutable/assets/EmptyState.CwKsu57Y.css +1 -0
  32. package/admin-build/_app/immutable/assets/Input.BDUSenmU.css +1 -0
  33. package/admin-build/_app/immutable/assets/Modal.Dm5B0Xie.css +1 -0
  34. package/admin-build/_app/immutable/assets/PageShell.CmU-Xh-b.css +1 -0
  35. package/admin-build/_app/immutable/assets/SchemaFieldEditor.g4NsCdno.css +1 -0
  36. package/admin-build/_app/immutable/assets/Select.BW4Keufm.css +1 -0
  37. package/admin-build/_app/immutable/assets/Skeleton.KWUulTKJ.css +1 -0
  38. package/admin-build/_app/immutable/assets/Tabs.CniGYb67.css +1 -0
  39. package/admin-build/_app/immutable/assets/TimeChart.BTCDAvmT.css +1 -0
  40. package/admin-build/_app/immutable/assets/Toggle.Cy_K12OM.css +1 -0
  41. package/admin-build/_app/immutable/assets/TopList.ClFzmPlA.css +1 -0
  42. package/admin-build/_app/immutable/chunks/7B47DvSx.js +1 -0
  43. package/admin-build/_app/immutable/chunks/7f08Id8e.js +1 -0
  44. package/admin-build/_app/immutable/chunks/8wJeQ7LN.js +1 -0
  45. package/admin-build/_app/immutable/chunks/B-h2afW5.js +1 -0
  46. package/admin-build/_app/immutable/chunks/B8vJP3wz.js +1 -0
  47. package/admin-build/_app/immutable/chunks/BR_fL5Yv.js +1 -0
  48. package/admin-build/_app/immutable/chunks/BY92tFS2.js +1 -0
  49. package/admin-build/_app/immutable/chunks/BcR-Rdj9.js +1 -0
  50. package/admin-build/_app/immutable/chunks/BdrwyZv8.js +1 -0
  51. package/admin-build/_app/immutable/chunks/Bh56EfQ_.js +1 -0
  52. package/admin-build/_app/immutable/chunks/BkrCkgYp.js +1 -0
  53. package/admin-build/_app/immutable/chunks/BmRjiP5k.js +1 -0
  54. package/admin-build/_app/immutable/chunks/BsokvhWC.js +1 -0
  55. package/admin-build/_app/immutable/chunks/C4D51vTW.js +1 -0
  56. package/admin-build/_app/immutable/chunks/C6puvcoR.js +2 -0
  57. package/admin-build/_app/immutable/chunks/CCKNu7m7.js +1 -0
  58. package/admin-build/_app/immutable/chunks/CWj6FrbW.js +1 -0
  59. package/admin-build/_app/immutable/chunks/Ce-ngf4p.js +5 -0
  60. package/admin-build/_app/immutable/chunks/Cs0GwzJA.js +1 -0
  61. package/admin-build/_app/immutable/chunks/CwROoZK0.js +1 -0
  62. package/admin-build/_app/immutable/chunks/CxCPv_Ut.js +1 -0
  63. package/admin-build/_app/immutable/chunks/CxbRue-5.js +1 -0
  64. package/admin-build/_app/immutable/chunks/CyqB6g-D.js +1 -0
  65. package/admin-build/_app/immutable/chunks/D5h5A1cc.js +2 -0
  66. package/admin-build/_app/immutable/chunks/DnyL7Zq-.js +1 -0
  67. package/admin-build/_app/immutable/chunks/DoPXzH7F.js +1 -0
  68. package/admin-build/_app/immutable/chunks/DrQSgw-f.js +1 -0
  69. package/admin-build/_app/immutable/chunks/DttM2zNO.js +1 -0
  70. package/admin-build/_app/immutable/chunks/DuXuUBWN.js +1 -0
  71. package/admin-build/_app/immutable/chunks/MdeqaOQx.js +10 -0
  72. package/admin-build/_app/immutable/chunks/NuUjtcO2.js +1 -0
  73. package/admin-build/_app/immutable/chunks/Q2nPFxS6.js +1 -0
  74. package/admin-build/_app/immutable/chunks/R6arueIl.js +1 -0
  75. package/admin-build/_app/immutable/chunks/UUazaC_N.js +1 -0
  76. package/admin-build/_app/immutable/chunks/cOYbrQxx.js +1 -0
  77. package/admin-build/_app/immutable/chunks/eFQHTGwA.js +1 -0
  78. package/admin-build/_app/immutable/chunks/ehbppgYb.js +1 -0
  79. package/admin-build/_app/immutable/chunks/glwixJlP.js +1 -0
  80. package/admin-build/_app/immutable/chunks/vApWTCBs.js +1 -0
  81. package/admin-build/_app/immutable/chunks/w89G9Xpi.js +1 -0
  82. package/admin-build/_app/immutable/chunks/wJsUhbfZ.js +1 -0
  83. package/admin-build/_app/immutable/chunks/zfauFM8P.js +1 -0
  84. package/admin-build/_app/immutable/entry/app.CcO-Uos3.js +2 -0
  85. package/admin-build/_app/immutable/entry/start.COebYq3I.js +1 -0
  86. package/admin-build/_app/immutable/nodes/0.CjtHKU-6.js +1 -0
  87. package/admin-build/_app/immutable/nodes/1.DEisjlM0.js +1 -0
  88. package/admin-build/_app/immutable/nodes/10.CvhdyWVB.js +1 -0
  89. package/admin-build/_app/immutable/nodes/11.DjHqcOvy.js +1 -0
  90. package/admin-build/_app/immutable/nodes/12.mQLz4Mj_.js +1 -0
  91. package/admin-build/_app/immutable/nodes/13.CBonZZyP.js +110 -0
  92. package/admin-build/_app/immutable/nodes/14.d-oiZL0j.js +3 -0
  93. package/admin-build/_app/immutable/nodes/15.CKPQsUYF.js +1 -0
  94. package/admin-build/_app/immutable/nodes/16.wPzAPQGx.js +1 -0
  95. package/admin-build/_app/immutable/nodes/17.DayhKyEZ.js +1 -0
  96. package/admin-build/_app/immutable/nodes/18.DKwS0Ir0.js +1 -0
  97. package/admin-build/_app/immutable/nodes/19.wPzAPQGx.js +1 -0
  98. package/admin-build/_app/immutable/nodes/2.BKoKrw1i.js +1 -0
  99. package/admin-build/_app/immutable/nodes/20.BvIkkkrW.js +1 -0
  100. package/admin-build/_app/immutable/nodes/21.DMaFhdHk.js +128 -0
  101. package/admin-build/_app/immutable/nodes/22.3xdgwuK1.js +1 -0
  102. package/admin-build/_app/immutable/nodes/23.8Bvgjbsl.js +112 -0
  103. package/admin-build/_app/immutable/nodes/24.DzSSzRhG.js +2 -0
  104. package/admin-build/_app/immutable/nodes/25.9KKYBnAE.js +2 -0
  105. package/admin-build/_app/immutable/nodes/26.Bhn9dfhY.js +1 -0
  106. package/admin-build/_app/immutable/nodes/27.kRLiC24G.js +1 -0
  107. package/admin-build/_app/immutable/nodes/28.BVIN1-7N.js +1 -0
  108. package/admin-build/_app/immutable/nodes/29.3yabZWj4.js +1 -0
  109. package/admin-build/_app/immutable/nodes/3.BFtSOkX7.js +2 -0
  110. package/admin-build/_app/immutable/nodes/30.CyCQlwaP.js +1 -0
  111. package/admin-build/_app/immutable/nodes/31.C4LDXjES.js +1 -0
  112. package/admin-build/_app/immutable/nodes/4.CvbiMlCa.js +1 -0
  113. package/admin-build/_app/immutable/nodes/5.C6BLv2eM.js +1 -0
  114. package/admin-build/_app/immutable/nodes/6.BcXvfl2P.js +1 -0
  115. package/admin-build/_app/immutable/nodes/7.CIuqhPiK.js +1 -0
  116. package/admin-build/_app/immutable/nodes/8.BQOR_JfO.js +1 -0
  117. package/admin-build/_app/immutable/nodes/9.NZqXQxPy.js +1 -0
  118. package/admin-build/_app/version.json +1 -0
  119. package/admin-build/favicon.svg +26 -0
  120. package/admin-build/index.html +45 -0
  121. package/openapi.json +19543 -0
  122. package/package.json +66 -0
  123. package/src/__tests__/admin-assets.test.ts +55 -0
  124. package/src/__tests__/admin-data-routes.test.ts +488 -0
  125. package/src/__tests__/admin-db-target.test.ts +103 -0
  126. package/src/__tests__/admin-routing.test.ts +31 -0
  127. package/src/__tests__/admin-user-management.test.ts +311 -0
  128. package/src/__tests__/analytics-query.test.ts +75 -0
  129. package/src/__tests__/auth-d1.test.ts +749 -0
  130. package/src/__tests__/auth-db-adapter.test.ts +73 -0
  131. package/src/__tests__/auth-jwt.test.ts +440 -0
  132. package/src/__tests__/auth-oauth.test.ts +389 -0
  133. package/src/__tests__/auth-password.test.ts +367 -0
  134. package/src/__tests__/auth-redirect.test.ts +87 -0
  135. package/src/__tests__/backup-restore.test.ts +711 -0
  136. package/src/__tests__/broadcast.test.ts +128 -0
  137. package/src/__tests__/cli.test.ts +178 -0
  138. package/src/__tests__/cloudflare-realtime.test.ts +113 -0
  139. package/src/__tests__/config.test.ts +469 -0
  140. package/src/__tests__/cors.test.ts +154 -0
  141. package/src/__tests__/cron.test.ts +302 -0
  142. package/src/__tests__/d1-handler.test.ts +402 -0
  143. package/src/__tests__/d1-sql.test.ts +120 -0
  144. package/src/__tests__/database-live-config.test.ts +42 -0
  145. package/src/__tests__/database-live-emitter.test.ts +56 -0
  146. package/src/__tests__/database-live-filters.test.ts +63 -0
  147. package/src/__tests__/database-live-route.test.ts +113 -0
  148. package/src/__tests__/db-sql.test.ts +163 -0
  149. package/src/__tests__/do-lifecycle.test.ts +263 -0
  150. package/src/__tests__/do-router.test.ts +729 -0
  151. package/src/__tests__/email-provider.test.ts +128 -0
  152. package/src/__tests__/email-templates.test.ts +528 -0
  153. package/src/__tests__/error-format.test.ts +250 -0
  154. package/src/__tests__/field-ops.test.ts +242 -0
  155. package/src/__tests__/functions-context.test.ts +334 -0
  156. package/src/__tests__/functions-d1-proxy.test.ts +229 -0
  157. package/src/__tests__/functions-registry-runtime-config.test.ts +17 -0
  158. package/src/__tests__/functions-route.test.ts +139 -0
  159. package/src/__tests__/internal-request.test.ts +77 -0
  160. package/src/__tests__/log-writer.test.ts +44 -0
  161. package/src/__tests__/logger.test.ts +58 -0
  162. package/src/__tests__/meta-admin-proxy.test.ts +48 -0
  163. package/src/__tests__/meta-export-coverage.test.ts +191 -0
  164. package/src/__tests__/meta-route-registration.test.ts +47 -0
  165. package/src/__tests__/namespace-dump.test.ts +28 -0
  166. package/src/__tests__/oauth-providers.test.ts +337 -0
  167. package/src/__tests__/openapi-coverage.test.ts +144 -0
  168. package/src/__tests__/pagination.test.ts +59 -0
  169. package/src/__tests__/password-policy.test.ts +191 -0
  170. package/src/__tests__/plugin-migrations.test.ts +379 -0
  171. package/src/__tests__/postgres-batch-compat.test.ts +133 -0
  172. package/src/__tests__/postgres-dialect.test.ts +328 -0
  173. package/src/__tests__/postgres-executor.test.ts +79 -0
  174. package/src/__tests__/postgres-field-ops-compat.test.ts +222 -0
  175. package/src/__tests__/postgres-schema-init.test.ts +105 -0
  176. package/src/__tests__/postgres-table-utils.test.ts +107 -0
  177. package/src/__tests__/presence.test.ts +199 -0
  178. package/src/__tests__/provider.test.ts +550 -0
  179. package/src/__tests__/public-user-profile.test.ts +339 -0
  180. package/src/__tests__/push-handlers.test.ts +179 -0
  181. package/src/__tests__/push-provider.test.ts +80 -0
  182. package/src/__tests__/push-token.test.ts +418 -0
  183. package/src/__tests__/query.test.ts +771 -0
  184. package/src/__tests__/rate-limit.test.ts +260 -0
  185. package/src/__tests__/room-access-policy.test.ts +101 -0
  186. package/src/__tests__/room-handler-context.test.ts +130 -0
  187. package/src/__tests__/room-monitoring.test.ts +138 -0
  188. package/src/__tests__/room-runtime-routing.test.ts +222 -0
  189. package/src/__tests__/room.test.ts +254 -0
  190. package/src/__tests__/route-parser.test.ts +490 -0
  191. package/src/__tests__/rules.test.ts +234 -0
  192. package/src/__tests__/runtime-surface-accounting.test.ts +120 -0
  193. package/src/__tests__/scheduled.test.ts +80 -0
  194. package/src/__tests__/schema.test.ts +1273 -0
  195. package/src/__tests__/security-hardening.test.ts +312 -0
  196. package/src/__tests__/server.unit.test.ts +333 -0
  197. package/src/__tests__/service-key-db-proxy.test.ts +650 -0
  198. package/src/__tests__/service-key-provider-bypass.test.ts +138 -0
  199. package/src/__tests__/service-key.test.ts +757 -0
  200. package/src/__tests__/smoke-skip-report.test.ts +72 -0
  201. package/src/__tests__/sms-provider.test.ts +39 -0
  202. package/src/__tests__/sql-route.test.ts +218 -0
  203. package/src/__tests__/storage-hook-context.test.ts +115 -0
  204. package/src/__tests__/totp.test.ts +200 -0
  205. package/src/__tests__/uuid.test.ts +144 -0
  206. package/src/__tests__/validation.test.ts +773 -0
  207. package/src/__tests__/websocket-pending.test.ts +163 -0
  208. package/src/_functions-registry.ts +51 -0
  209. package/src/bench-entry.ts +9 -0
  210. package/src/cloudflare-test.d.ts +1 -0
  211. package/src/durable-objects/auth-do.ts +49 -0
  212. package/src/durable-objects/database-do.ts +2240 -0
  213. package/src/durable-objects/database-live-do.ts +949 -0
  214. package/src/durable-objects/logs-do.ts +1200 -0
  215. package/src/durable-objects/room-runtime-base.ts +1604 -0
  216. package/src/durable-objects/rooms-do.ts +2191 -0
  217. package/src/generated-config.ts +6 -0
  218. package/src/index.ts +382 -0
  219. package/src/lib/admin-assets.ts +54 -0
  220. package/src/lib/admin-db-target.ts +301 -0
  221. package/src/lib/admin-routing.ts +35 -0
  222. package/src/lib/admin-user-management.ts +464 -0
  223. package/src/lib/analytics-adapter.ts +103 -0
  224. package/src/lib/analytics-query.ts +579 -0
  225. package/src/lib/auth-d1-service.ts +1193 -0
  226. package/src/lib/auth-d1.ts +1056 -0
  227. package/src/lib/auth-db-adapter.ts +289 -0
  228. package/src/lib/auth-redirect.ts +116 -0
  229. package/src/lib/cidr.ts +115 -0
  230. package/src/lib/client-ip.ts +51 -0
  231. package/src/lib/cloudflare-realtime.ts +251 -0
  232. package/src/lib/control-db.ts +36 -0
  233. package/src/lib/cron.ts +163 -0
  234. package/src/lib/d1-handler.ts +1425 -0
  235. package/src/lib/d1-schema-init.ts +255 -0
  236. package/src/lib/d1-sql.ts +33 -0
  237. package/src/lib/database-live-config.ts +24 -0
  238. package/src/lib/database-live-emitter.ts +111 -0
  239. package/src/lib/db-sql.ts +66 -0
  240. package/src/lib/do-retry.ts +36 -0
  241. package/src/lib/do-router.ts +270 -0
  242. package/src/lib/do-sql.ts +73 -0
  243. package/src/lib/email-provider.ts +379 -0
  244. package/src/lib/email-templates.ts +285 -0
  245. package/src/lib/email-translations.ts +422 -0
  246. package/src/lib/errors.ts +151 -0
  247. package/src/lib/functions.ts +2091 -0
  248. package/src/lib/hono.ts +56 -0
  249. package/src/lib/internal-request.ts +56 -0
  250. package/src/lib/jwt.ts +354 -0
  251. package/src/lib/log-writer.ts +272 -0
  252. package/src/lib/namespace-dump.ts +125 -0
  253. package/src/lib/oauth-providers.ts +1225 -0
  254. package/src/lib/op-parser.ts +99 -0
  255. package/src/lib/openapi.ts +146 -0
  256. package/src/lib/pagination.ts +19 -0
  257. package/src/lib/password-policy.ts +102 -0
  258. package/src/lib/password.ts +145 -0
  259. package/src/lib/plugin-migrations.ts +612 -0
  260. package/src/lib/postgres-executor.ts +203 -0
  261. package/src/lib/postgres-handler.ts +1102 -0
  262. package/src/lib/postgres-schema-init.ts +341 -0
  263. package/src/lib/postgres-table-utils.ts +87 -0
  264. package/src/lib/public-user-profile.ts +187 -0
  265. package/src/lib/push-provider.ts +409 -0
  266. package/src/lib/push-token.ts +294 -0
  267. package/src/lib/query-engine.ts +768 -0
  268. package/src/lib/room-monitoring.ts +97 -0
  269. package/src/lib/room-runtime.ts +14 -0
  270. package/src/lib/route-parser.ts +434 -0
  271. package/src/lib/schema.ts +538 -0
  272. package/src/lib/schemas.ts +152 -0
  273. package/src/lib/service-key.ts +419 -0
  274. package/src/lib/sms-provider.ts +230 -0
  275. package/src/lib/startup-config.ts +99 -0
  276. package/src/lib/totp.ts +242 -0
  277. package/src/lib/uuid.ts +87 -0
  278. package/src/lib/validation.ts +205 -0
  279. package/src/lib/version.ts +2 -0
  280. package/src/lib/websocket-pending.ts +40 -0
  281. package/src/middleware/auth.ts +169 -0
  282. package/src/middleware/captcha-verify.ts +217 -0
  283. package/src/middleware/cors.ts +159 -0
  284. package/src/middleware/error-handler.ts +54 -0
  285. package/src/middleware/internal-guard.ts +26 -0
  286. package/src/middleware/logger.ts +126 -0
  287. package/src/middleware/rate-limit.ts +283 -0
  288. package/src/middleware/rules.ts +475 -0
  289. package/src/routes/admin-auth.ts +447 -0
  290. package/src/routes/admin.ts +3501 -0
  291. package/src/routes/analytics-api.ts +290 -0
  292. package/src/routes/auth.ts +4222 -0
  293. package/src/routes/backup.ts +1466 -0
  294. package/src/routes/config.ts +53 -0
  295. package/src/routes/d1.ts +109 -0
  296. package/src/routes/database-live.ts +281 -0
  297. package/src/routes/functions.ts +155 -0
  298. package/src/routes/health.ts +32 -0
  299. package/src/routes/kv.ts +167 -0
  300. package/src/routes/oauth.ts +1055 -0
  301. package/src/routes/push.ts +1465 -0
  302. package/src/routes/room.ts +639 -0
  303. package/src/routes/schema-endpoint.ts +76 -0
  304. package/src/routes/sql.ts +176 -0
  305. package/src/routes/storage.ts +1674 -0
  306. package/src/routes/tables.ts +699 -0
  307. package/src/routes/users.ts +21 -0
  308. package/src/routes/vectorize.ts +372 -0
  309. package/src/types.ts +99 -0
@@ -0,0 +1,773 @@
1
+ /**
2
+ * 서버 단위 테스트 — lib/validation.ts + lib/cidr.ts
3
+ * 1-20 rate-limit.test.ts (일부) + 필드 validation 테스트 — 70개
4
+ *
5
+ * 실행: cd packages/server && npx vitest run src/__tests__/validation.test.ts
6
+ *
7
+ * 테스트 대상:
8
+ * validateInsert — required / type / enum / pattern / min / max / unknown field
9
+ * validateUpdate — partial update / $op passthrough / required null rejection
10
+ * isFieldOperator — $op 객체 판별
11
+ * isIpInCidr — IPv4/IPv6 CIDR 범위 검사
12
+ */
13
+
14
+ import { describe, it, expect } from 'vitest';
15
+ import {
16
+ CUSTOM_RECORD_ID_MESSAGE,
17
+ CUSTOM_RECORD_ID_PATTERN,
18
+ validateCustomRecordId,
19
+ validateInsert,
20
+ validateUpdate,
21
+ isFieldOperator,
22
+ summarizeValidationErrors,
23
+ } from '../lib/validation.js';
24
+ import { isIpInCidr } from '../lib/cidr.js';
25
+
26
+ // ─── A. validateInsert ────────────────────────────────────────────────────────
27
+
28
+ describe('validateInsert', () => {
29
+ it('no schema → valid (schemaless)', () => {
30
+ const result = validateInsert({ anything: 'goes' }, undefined);
31
+ expect(result.valid).toBe(true);
32
+ expect(result.errors).toEqual({});
33
+ });
34
+
35
+ it('empty schema → unknown fields silently ignored → valid', () => {
36
+ const result = validateInsert({ title: 'hello' }, {});
37
+ // Unknown fields are silently ignored — the SQL layer filters them out.
38
+ // Rejecting unknown fields here would break SDK payloads that send extra metadata.
39
+ expect(result.valid).toBe(true);
40
+ expect(result.errors).toEqual({});
41
+ });
42
+
43
+ it('required field present → valid', () => {
44
+ const result = validateInsert(
45
+ { email: 'test@example.com' },
46
+ { email: { type: 'string', required: true } },
47
+ );
48
+ expect(result.valid).toBe(true);
49
+ });
50
+
51
+ it('required field missing → error', () => {
52
+ const result = validateInsert(
53
+ {},
54
+ { email: { type: 'string', required: true } },
55
+ );
56
+ expect(result.valid).toBe(false);
57
+ expect(result.errors.email).toContain('required');
58
+ });
59
+
60
+ it('required field with default → valid even if missing', () => {
61
+ const result = validateInsert(
62
+ {},
63
+ { role: { type: 'string', required: true, default: 'user' } },
64
+ );
65
+ expect(result.valid).toBe(true);
66
+ });
67
+
68
+ it('string type — string value → valid', () => {
69
+ const result = validateInsert({ name: 'Alice' }, { name: { type: 'string' } });
70
+ expect(result.valid).toBe(true);
71
+ });
72
+
73
+ it('string type — number value → invalid', () => {
74
+ const result = validateInsert({ name: 42 }, { name: { type: 'string' } });
75
+ expect(result.valid).toBe(false);
76
+ expect(result.errors.name).toContain('string');
77
+ });
78
+
79
+ it('number type — number value → valid', () => {
80
+ const result = validateInsert({ age: 25 }, { age: { type: 'number' } });
81
+ expect(result.valid).toBe(true);
82
+ });
83
+
84
+ it('number type — string value → invalid', () => {
85
+ const result = validateInsert({ age: 'twenty-five' }, { age: { type: 'number' } });
86
+ expect(result.valid).toBe(false);
87
+ expect(result.errors.age).toContain('number');
88
+ });
89
+
90
+ it('boolean type — boolean value → valid', () => {
91
+ const result = validateInsert({ active: true }, { active: { type: 'boolean' } });
92
+ expect(result.valid).toBe(true);
93
+ });
94
+
95
+ it('boolean type — string value → invalid', () => {
96
+ const result = validateInsert({ active: 'true' }, { active: { type: 'boolean' } });
97
+ expect(result.valid).toBe(false);
98
+ });
99
+
100
+ it('datetime type — ISO string → valid', () => {
101
+ const result = validateInsert(
102
+ { ts: '2024-01-01T00:00:00Z' },
103
+ { ts: { type: 'datetime' } },
104
+ );
105
+ expect(result.valid).toBe(true);
106
+ });
107
+
108
+ it('datetime type — invalid date string → invalid', () => {
109
+ const result = validateInsert({ ts: 'not-a-date' }, { ts: { type: 'datetime' } });
110
+ expect(result.valid).toBe(false);
111
+ });
112
+
113
+ it('json type — any value → valid', () => {
114
+ const result = validateInsert({ meta: { key: 'value' } }, { meta: { type: 'json' } });
115
+ expect(result.valid).toBe(true);
116
+ });
117
+
118
+ it('string min constraint', () => {
119
+ const result = validateInsert({ pw: 'abc' }, { pw: { type: 'string', min: 8 } });
120
+ expect(result.valid).toBe(false);
121
+ expect(result.errors.pw).toContain('characters');
122
+ });
123
+
124
+ it('string max constraint', () => {
125
+ const result = validateInsert({ pw: 'a'.repeat(300) }, { pw: { type: 'string', max: 100 } });
126
+ expect(result.valid).toBe(false);
127
+ expect(result.errors.pw).toContain('characters');
128
+ });
129
+
130
+ it('string pattern constraint', () => {
131
+ const result = validateInsert(
132
+ { slug: 'invalid slug!' },
133
+ { slug: { type: 'string', pattern: '^[a-z0-9-]+$' } },
134
+ );
135
+ expect(result.valid).toBe(false);
136
+ expect(result.errors.slug).toContain('pattern');
137
+ });
138
+
139
+ it('string enum constraint — valid value', () => {
140
+ const result = validateInsert(
141
+ { role: 'admin' },
142
+ { role: { type: 'string', enum: ['user', 'admin', 'mod'] } },
143
+ );
144
+ expect(result.valid).toBe(true);
145
+ });
146
+
147
+ it('string enum constraint — invalid value', () => {
148
+ const result = validateInsert(
149
+ { role: 'superuser' },
150
+ { role: { type: 'string', enum: ['user', 'admin'] } },
151
+ );
152
+ expect(result.valid).toBe(false);
153
+ expect(result.errors.role).toContain('one of');
154
+ });
155
+
156
+ it('number min constraint', () => {
157
+ const result = validateInsert({ age: -1 }, { age: { type: 'number', min: 0 } });
158
+ expect(result.valid).toBe(false);
159
+ expect(result.errors.age).toContain('at least');
160
+ });
161
+
162
+ it('number max constraint', () => {
163
+ const result = validateInsert({ age: 200 }, { age: { type: 'number', max: 150 } });
164
+ expect(result.valid).toBe(false);
165
+ expect(result.errors.age).toContain('at most');
166
+ });
167
+
168
+ it('unknown field → silently ignored (valid)', () => {
169
+ const result = validateInsert(
170
+ { title: 'ok', unknown_field: 'bad' },
171
+ { title: { type: 'string' } },
172
+ );
173
+ expect(result.valid).toBe(true);
174
+ expect(Object.keys(result.errors)).toHaveLength(0);
175
+ });
176
+
177
+ it('optional field absent → valid', () => {
178
+ const result = validateInsert({}, { nickname: { type: 'string' } });
179
+ expect(result.valid).toBe(true);
180
+ });
181
+
182
+ it('custom record id with Korean characters → invalid', () => {
183
+ const result = validateInsert({ id: '한글-id' }, { title: { type: 'string' } });
184
+ expect(result.valid).toBe(false);
185
+ expect(result.errors.id).toContain('English letters');
186
+ });
187
+
188
+ it('NaN for number → invalid', () => {
189
+ const result = validateInsert({ n: NaN }, { n: { type: 'number' } });
190
+ expect(result.valid).toBe(false);
191
+ });
192
+ });
193
+
194
+ describe('validateCustomRecordId', () => {
195
+ it('blank id is allowed and will be auto-generated later', () => {
196
+ expect(validateCustomRecordId('')).toBeNull();
197
+ });
198
+
199
+ it('ASCII-safe custom id is valid', () => {
200
+ expect(validateCustomRecordId('post_123-abc')).toBeNull();
201
+ });
202
+
203
+ it('exports the shared record id pattern for reuse', () => {
204
+ expect(CUSTOM_RECORD_ID_PATTERN.test('post_123-abc')).toBe(true);
205
+ expect(CUSTOM_RECORD_ID_PATTERN.test('한글-id')).toBe(false);
206
+ });
207
+
208
+ it('uses the shared invalid record id message', () => {
209
+ expect(validateCustomRecordId('한글-id')).toBe(CUSTOM_RECORD_ID_MESSAGE);
210
+ });
211
+ });
212
+
213
+ describe('summarizeValidationErrors', () => {
214
+ it('uses a clearer message for record id validation', () => {
215
+ expect(
216
+ summarizeValidationErrors({
217
+ id: 'Record ID must use English letters, numbers, hyphen (-), or underscore (_).',
218
+ }),
219
+ ).toBe("Invalid record ID. Record ID must use English letters, numbers, hyphen (-), or underscore (_).");
220
+ });
221
+ });
222
+
223
+ // ─── B. validateUpdate ────────────────────────────────────────────────────────
224
+
225
+ describe('validateUpdate', () => {
226
+ it('no schema → valid (schemaless)', () => {
227
+ const result = validateUpdate({ x: 1 }, undefined);
228
+ expect(result.valid).toBe(true);
229
+ });
230
+
231
+ it('valid partial update', () => {
232
+ const result = validateUpdate(
233
+ { title: 'New Title' },
234
+ { title: { type: 'string' }, status: { type: 'string' } },
235
+ );
236
+ expect(result.valid).toBe(true);
237
+ });
238
+
239
+ it('unknown field in update → silently ignored (valid)', () => {
240
+ const result = validateUpdate(
241
+ { unknownField: 'x' },
242
+ { title: { type: 'string' } },
243
+ );
244
+ expect(result.valid).toBe(true);
245
+ expect(Object.keys(result.errors)).toHaveLength(0);
246
+ });
247
+
248
+ it('$op increment passes through without type error', () => {
249
+ const result = validateUpdate(
250
+ { count: { $op: 'increment', value: 1 } },
251
+ { count: { type: 'number' } },
252
+ );
253
+ expect(result.valid).toBe(true);
254
+ });
255
+
256
+ it('$op deleteField passes through', () => {
257
+ const result = validateUpdate(
258
+ { avatar: { $op: 'deleteField' } },
259
+ { avatar: { type: 'string' } },
260
+ );
261
+ expect(result.valid).toBe(true);
262
+ });
263
+
264
+ it('$op deleteField on required field → error', () => {
265
+ const result = validateUpdate(
266
+ { email: { $op: 'deleteField' } },
267
+ { email: { type: 'string', required: true } },
268
+ );
269
+ expect(result.valid).toBe(false);
270
+ expect(result.errors.email).toContain('required');
271
+ });
272
+
273
+ it('required field set to null → error', () => {
274
+ const result = validateUpdate(
275
+ { email: null },
276
+ { email: { type: 'string', required: true } },
277
+ );
278
+ expect(result.valid).toBe(false);
279
+ expect(result.errors.email).toContain('required');
280
+ });
281
+
282
+ it('auto fields (id/createdAt/updatedAt) always allowed', () => {
283
+ const result = validateUpdate(
284
+ { id: 'new-id', createdAt: '...', updatedAt: '...' },
285
+ { title: { type: 'string' } },
286
+ );
287
+ expect(result.valid).toBe(true);
288
+ });
289
+
290
+ it('wrong type in partial update → error', () => {
291
+ const result = validateUpdate(
292
+ { views: 'not-a-number' },
293
+ { views: { type: 'number' } },
294
+ );
295
+ expect(result.valid).toBe(false);
296
+ });
297
+ });
298
+
299
+ // ─── C. isFieldOperator ───────────────────────────────────────────────────────
300
+
301
+ describe('isFieldOperator', () => {
302
+ it('{ $op: "increment", value: 5 } → true', () => {
303
+ expect(isFieldOperator({ $op: 'increment', value: 5 })).toBe(true);
304
+ });
305
+
306
+ it('{ $op: "deleteField" } → true', () => {
307
+ expect(isFieldOperator({ $op: 'deleteField' })).toBe(true);
308
+ });
309
+
310
+ it('null → false', () => {
311
+ expect(isFieldOperator(null)).toBe(false);
312
+ });
313
+
314
+ it('string → false', () => {
315
+ expect(isFieldOperator('increment')).toBe(false);
316
+ });
317
+
318
+ it('number → false', () => {
319
+ expect(isFieldOperator(42)).toBe(false);
320
+ });
321
+
322
+ it('plain object without $op → false', () => {
323
+ expect(isFieldOperator({ op: 'increment' })).toBe(false);
324
+ });
325
+
326
+ it('array → false', () => {
327
+ expect(isFieldOperator([])).toBe(false);
328
+ });
329
+
330
+ it('$op is a number → false-ish (since $op must be string)', () => {
331
+ const result = isFieldOperator({ $op: 42 });
332
+ expect(result).toBe(false);
333
+ });
334
+ });
335
+
336
+ // ─── D. isIpInCidr — IPv4 ────────────────────────────────────────────────────
337
+
338
+ describe('isIpInCidr — IPv4', () => {
339
+ it('10.0.0.1 in 10.0.0.0/8 → true', () => {
340
+ expect(isIpInCidr('10.0.0.1', '10.0.0.0/8')).toBe(true);
341
+ });
342
+
343
+ it('10.0.0.255 in 10.0.0.0/8 → true', () => {
344
+ expect(isIpInCidr('10.0.0.255', '10.0.0.0/8')).toBe(true);
345
+ });
346
+
347
+ it('192.168.1.1 in 10.0.0.0/8 → false', () => {
348
+ expect(isIpInCidr('192.168.1.1', '10.0.0.0/8')).toBe(false);
349
+ });
350
+
351
+ it('192.168.1.100 in 192.168.1.0/24 → true', () => {
352
+ expect(isIpInCidr('192.168.1.100', '192.168.1.0/24')).toBe(true);
353
+ });
354
+
355
+ it('192.168.2.1 in 192.168.1.0/24 → false', () => {
356
+ expect(isIpInCidr('192.168.2.1', '192.168.1.0/24')).toBe(false);
357
+ });
358
+
359
+ it('127.0.0.1 in 127.0.0.1/32 → true (exact match)', () => {
360
+ expect(isIpInCidr('127.0.0.1', '127.0.0.1/32')).toBe(true);
361
+ });
362
+
363
+ it('any IP in 0.0.0.0/0 → true', () => {
364
+ expect(isIpInCidr('8.8.8.8', '0.0.0.0/0')).toBe(true);
365
+ });
366
+
367
+ it('no CIDR slash → false', () => {
368
+ expect(isIpInCidr('10.0.0.1', '10.0.0.0')).toBe(false);
369
+ });
370
+
371
+ it('invalid IP → false', () => {
372
+ expect(isIpInCidr('invalid.ip', '10.0.0.0/8')).toBe(false);
373
+ });
374
+
375
+ it('prefix > 32 → false', () => {
376
+ expect(isIpInCidr('10.0.0.1', '10.0.0.0/33')).toBe(false);
377
+ });
378
+
379
+ it('IPv4 vs IPv6 CIDR → false (version mismatch)', () => {
380
+ expect(isIpInCidr('10.0.0.1', '::1/128')).toBe(false);
381
+ });
382
+ });
383
+
384
+ // ─── E. isIpInCidr — IPv6 ────────────────────────────────────────────────────
385
+
386
+ describe('isIpInCidr — IPv6', () => {
387
+ it('::1 in ::1/128 → true', () => {
388
+ expect(isIpInCidr('::1', '::1/128')).toBe(true);
389
+ });
390
+
391
+ it('2001:db8::1 in 2001:db8::/32 → true', () => {
392
+ expect(isIpInCidr('2001:db8::1', '2001:db8::/32')).toBe(true);
393
+ });
394
+
395
+ it('2001:db9::1 in 2001:db8::/32 → false', () => {
396
+ expect(isIpInCidr('2001:db9::1', '2001:db8::/32')).toBe(false);
397
+ });
398
+
399
+ it('IPv6 in IPv4 CIDR → false', () => {
400
+ expect(isIpInCidr('::1', '10.0.0.0/8')).toBe(false);
401
+ });
402
+
403
+ it('prefix > 128 → false', () => {
404
+ expect(isIpInCidr('::1', '::1/129')).toBe(false);
405
+ });
406
+ });
407
+
408
+ // ─── F. isIpInCidr — Edge Cases (mutation coverage) ─────────────────────────
409
+
410
+ describe('isIpInCidr — IPv4 prefix boundaries', () => {
411
+ it('/31 → 2 addresses: .0 included', () => {
412
+ expect(isIpInCidr('192.168.0.0', '192.168.0.0/31')).toBe(true);
413
+ });
414
+
415
+ it('/31 → 2 addresses: .1 included', () => {
416
+ expect(isIpInCidr('192.168.0.1', '192.168.0.0/31')).toBe(true);
417
+ });
418
+
419
+ it('/31 → 2 addresses: .2 excluded', () => {
420
+ expect(isIpInCidr('192.168.0.2', '192.168.0.0/31')).toBe(false);
421
+ });
422
+
423
+ it('/30 → 4 addresses: .3 included, .4 excluded', () => {
424
+ expect(isIpInCidr('192.168.0.3', '192.168.0.0/30')).toBe(true);
425
+ expect(isIpInCidr('192.168.0.4', '192.168.0.0/30')).toBe(false);
426
+ });
427
+
428
+ it('/25 → upper half boundary', () => {
429
+ expect(isIpInCidr('192.168.1.127', '192.168.1.0/25')).toBe(true);
430
+ expect(isIpInCidr('192.168.1.128', '192.168.1.0/25')).toBe(false);
431
+ });
432
+
433
+ it('/16 → Class B boundary', () => {
434
+ expect(isIpInCidr('172.16.255.255', '172.16.0.0/16')).toBe(true);
435
+ expect(isIpInCidr('172.17.0.0', '172.16.0.0/16')).toBe(false);
436
+ });
437
+
438
+ it('/1 → entire upper/lower half', () => {
439
+ expect(isIpInCidr('0.0.0.1', '0.0.0.0/1')).toBe(true);
440
+ expect(isIpInCidr('127.255.255.255', '0.0.0.0/1')).toBe(true);
441
+ expect(isIpInCidr('128.0.0.0', '0.0.0.0/1')).toBe(false);
442
+ });
443
+ });
444
+
445
+ describe('isIpInCidr — IPv4 parse edge cases', () => {
446
+ it('leading zeros rejected (octal attack prevention)', () => {
447
+ expect(isIpInCidr('010.0.0.1', '10.0.0.0/8')).toBe(false);
448
+ });
449
+
450
+ it('too many octets → false', () => {
451
+ expect(isIpInCidr('10.0.0.1.1', '10.0.0.0/8')).toBe(false);
452
+ });
453
+
454
+ it('too few octets → false', () => {
455
+ expect(isIpInCidr('10.0.0', '10.0.0.0/8')).toBe(false);
456
+ });
457
+
458
+ it('negative octet → false', () => {
459
+ expect(isIpInCidr('10.-1.0.1', '10.0.0.0/8')).toBe(false);
460
+ });
461
+
462
+ it('256 in octet → false', () => {
463
+ expect(isIpInCidr('10.0.0.256', '10.0.0.0/8')).toBe(false);
464
+ });
465
+
466
+ it('non-numeric octet → false', () => {
467
+ expect(isIpInCidr('10.0.abc.1', '10.0.0.0/8')).toBe(false);
468
+ });
469
+
470
+ it('negative prefix → false', () => {
471
+ expect(isIpInCidr('10.0.0.1', '10.0.0.0/-1')).toBe(false);
472
+ });
473
+
474
+ it('NaN prefix → false', () => {
475
+ expect(isIpInCidr('10.0.0.1', '10.0.0.0/abc')).toBe(false);
476
+ });
477
+
478
+ it('invalid CIDR IP → false', () => {
479
+ expect(isIpInCidr('10.0.0.1', 'invalid/8')).toBe(false);
480
+ });
481
+ });
482
+
483
+ describe('isIpInCidr — IPv6 prefix boundaries', () => {
484
+ it('/127 → 2 addresses', () => {
485
+ expect(isIpInCidr('2001:db8::0', '2001:db8::/127')).toBe(true);
486
+ expect(isIpInCidr('2001:db8::1', '2001:db8::/127')).toBe(true);
487
+ expect(isIpInCidr('2001:db8::2', '2001:db8::/127')).toBe(false);
488
+ });
489
+
490
+ it('/64 → network prefix boundary', () => {
491
+ expect(isIpInCidr('2001:db8:0:0:ffff:ffff:ffff:ffff', '2001:db8::/64')).toBe(true);
492
+ expect(isIpInCidr('2001:db8:0:1::', '2001:db8::/64')).toBe(false);
493
+ });
494
+
495
+ it('/0 → all IPv6 addresses match', () => {
496
+ expect(isIpInCidr('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', '::/0')).toBe(true);
497
+ expect(isIpInCidr('::', '::/0')).toBe(true);
498
+ });
499
+
500
+ it(':: at end of CIDR → valid', () => {
501
+ expect(isIpInCidr('2001:db8::1', '2001:db8::/32')).toBe(true);
502
+ });
503
+
504
+ it('full IPv6 notation matches :: shorthand CIDR', () => {
505
+ expect(isIpInCidr('0000:0000:0000:0000:0000:0000:0000:0001', '::1/128')).toBe(true);
506
+ });
507
+ });
508
+
509
+ describe('isIpInCidr — IPv6 parse edge cases', () => {
510
+ it('multiple :: → false (invalid IPv6)', () => {
511
+ expect(isIpInCidr('1::2::3', '1::2/128')).toBe(false);
512
+ });
513
+
514
+ it('too many groups (9 groups) → false', () => {
515
+ expect(isIpInCidr('1:2:3:4:5:6:7:8:9', '1:2:3:4:5:6:7:8/128')).toBe(false);
516
+ });
517
+
518
+ it('group value > 0xffff → false', () => {
519
+ expect(isIpInCidr('10000::1', '10000::/128')).toBe(false);
520
+ });
521
+
522
+ it('invalid hex chars → false', () => {
523
+ expect(isIpInCidr('gggg::1', '::1/128')).toBe(false);
524
+ });
525
+
526
+ it('empty string IP → false', () => {
527
+ expect(isIpInCidr('', '::1/128')).toBe(false);
528
+ });
529
+
530
+ it('prefix exactly 0 → match all of same version', () => {
531
+ expect(isIpInCidr('::1', '::/0')).toBe(true);
532
+ });
533
+
534
+ it('prefix exactly 128 → exact match only', () => {
535
+ expect(isIpInCidr('::1', '::1/128')).toBe(true);
536
+ expect(isIpInCidr('::2', '::1/128')).toBe(false);
537
+ });
538
+ });
539
+
540
+ // ─── Mutation-killing: validation edge cases ────────────────────────────────
541
+
542
+ describe('validateInsert — mutation-killing', () => {
543
+ it('schemaless insert returns valid=true AND errors={}', () => {
544
+ const result = validateInsert({ anything: 'goes' });
545
+ expect(result).toEqual({ valid: true, errors: {} });
546
+ });
547
+
548
+ it('id field in data is silently skipped (auto-managed)', () => {
549
+ const result = validateInsert(
550
+ { id: 'custom-id', title: 'hello' },
551
+ { title: { type: 'string' } },
552
+ );
553
+ expect(result.valid).toBe(true);
554
+ expect(result.errors).not.toHaveProperty('id');
555
+ });
556
+
557
+ it('required field with null value → invalid', () => {
558
+ const result = validateInsert(
559
+ { name: null },
560
+ { name: { type: 'string', required: true } },
561
+ );
562
+ expect(result.valid).toBe(false);
563
+ expect(result.errors.name).toContain('required');
564
+ });
565
+
566
+ it('optional field with null value → valid (skipped)', () => {
567
+ const result = validateInsert(
568
+ { bio: null },
569
+ { bio: { type: 'string' } },
570
+ );
571
+ expect(result.valid).toBe(true);
572
+ });
573
+
574
+ it('optional field with undefined value → valid (skipped)', () => {
575
+ const result = validateInsert(
576
+ {},
577
+ { bio: { type: 'string' } },
578
+ );
579
+ expect(result.valid).toBe(true);
580
+ });
581
+
582
+ it('string at exactly min length → valid', () => {
583
+ const result = validateInsert(
584
+ { name: 'ab' },
585
+ { name: { type: 'string', min: 2 } },
586
+ );
587
+ expect(result.valid).toBe(true);
588
+ });
589
+
590
+ it('string one below min length → invalid', () => {
591
+ const result = validateInsert(
592
+ { name: 'a' },
593
+ { name: { type: 'string', min: 2 } },
594
+ );
595
+ expect(result.valid).toBe(false);
596
+ });
597
+
598
+ it('string at exactly max length → valid', () => {
599
+ const result = validateInsert(
600
+ { name: 'abc' },
601
+ { name: { type: 'string', max: 3 } },
602
+ );
603
+ expect(result.valid).toBe(true);
604
+ });
605
+
606
+ it('string one above max length → invalid', () => {
607
+ const result = validateInsert(
608
+ { name: 'abcd' },
609
+ { name: { type: 'string', max: 3 } },
610
+ );
611
+ expect(result.valid).toBe(false);
612
+ });
613
+
614
+ it('enum join separator → error shows comma-space between values', () => {
615
+ const result = validateInsert(
616
+ { status: 'bad' },
617
+ { status: { type: 'string', enum: ['a', 'b', 'c'] } },
618
+ );
619
+ expect(result.errors.status).toBe('Must be one of: a, b, c');
620
+ });
621
+
622
+ it('number exactly at min → valid', () => {
623
+ const result = validateInsert(
624
+ { score: 10 },
625
+ { score: { type: 'number', min: 10 } },
626
+ );
627
+ expect(result.valid).toBe(true);
628
+ });
629
+
630
+ it('number one below min → invalid', () => {
631
+ const result = validateInsert(
632
+ { score: 9 },
633
+ { score: { type: 'number', min: 10 } },
634
+ );
635
+ expect(result.valid).toBe(false);
636
+ });
637
+
638
+ it('number exactly at max → valid', () => {
639
+ const result = validateInsert(
640
+ { score: 100 },
641
+ { score: { type: 'number', max: 100 } },
642
+ );
643
+ expect(result.valid).toBe(true);
644
+ });
645
+
646
+ it('number one above max → invalid', () => {
647
+ const result = validateInsert(
648
+ { score: 101 },
649
+ { score: { type: 'number', max: 100 } },
650
+ );
651
+ expect(result.valid).toBe(false);
652
+ });
653
+
654
+ it('number with both min and max, value in range → valid', () => {
655
+ const result = validateInsert(
656
+ { score: 50 },
657
+ { score: { type: 'number', min: 0, max: 100 } },
658
+ );
659
+ expect(result.valid).toBe(true);
660
+ });
661
+
662
+ it('number with no min/max constraint → valid for any number', () => {
663
+ const result = validateInsert(
664
+ { score: -999 },
665
+ { score: { type: 'number' } },
666
+ );
667
+ expect(result.valid).toBe(true);
668
+ });
669
+
670
+ it('datetime with invalid string → invalid', () => {
671
+ const result = validateInsert(
672
+ { date: 'not-a-date' },
673
+ { date: { type: 'datetime' } },
674
+ );
675
+ expect(result.valid).toBe(false);
676
+ expect(result.errors.date).toContain('Invalid datetime');
677
+ });
678
+
679
+ it('datetime with valid ISO string → valid', () => {
680
+ const result = validateInsert(
681
+ { date: '2024-01-15T10:30:00Z' },
682
+ { date: { type: 'datetime' } },
683
+ );
684
+ expect(result.valid).toBe(true);
685
+ });
686
+ });
687
+
688
+ describe('validateUpdate — mutation-killing', () => {
689
+ it('schemaless update returns valid=true AND errors={}', () => {
690
+ const result = validateUpdate({ anything: 'goes' });
691
+ expect(result).toEqual({ valid: true, errors: {} });
692
+ });
693
+
694
+ it('id field in data is skipped (auto-managed)', () => {
695
+ const result = validateUpdate(
696
+ { id: 'new-id', title: 'updated' },
697
+ { title: { type: 'string' } },
698
+ );
699
+ expect(result.valid).toBe(true);
700
+ });
701
+
702
+ it('null value for optional field → valid (skipped)', () => {
703
+ const result = validateUpdate(
704
+ { bio: null },
705
+ { bio: { type: 'string' } },
706
+ );
707
+ expect(result.valid).toBe(true);
708
+ });
709
+
710
+ it('undefined value for field → valid (skipped)', () => {
711
+ const result = validateUpdate(
712
+ { bio: undefined },
713
+ { bio: { type: 'string' } },
714
+ );
715
+ expect(result.valid).toBe(true);
716
+ });
717
+
718
+ it('null for required field → invalid', () => {
719
+ const result = validateUpdate(
720
+ { name: null },
721
+ { name: { type: 'string', required: true } },
722
+ );
723
+ expect(result.valid).toBe(false);
724
+ });
725
+
726
+ it('undefined for required field → invalid', () => {
727
+ const result = validateUpdate(
728
+ { name: undefined },
729
+ { name: { type: 'string', required: true } },
730
+ );
731
+ expect(result.valid).toBe(false);
732
+ });
733
+ });
734
+
735
+ describe('isIpInCidr — mask boundary precision', () => {
736
+ it('IPv4 /31 boundary: .0 and .1 match, .2 does not', () => {
737
+ expect(isIpInCidr('192.168.1.0', '192.168.1.0/31')).toBe(true);
738
+ expect(isIpInCidr('192.168.1.1', '192.168.1.0/31')).toBe(true);
739
+ expect(isIpInCidr('192.168.1.2', '192.168.1.0/31')).toBe(false);
740
+ });
741
+
742
+ it('IPv4 /25 boundary: .127 matches, .128 does not', () => {
743
+ expect(isIpInCidr('10.0.0.127', '10.0.0.0/25')).toBe(true);
744
+ expect(isIpInCidr('10.0.0.128', '10.0.0.0/25')).toBe(false);
745
+ });
746
+
747
+ it('IPv4 /1 boundary: 127.x matches, 128.x does not', () => {
748
+ expect(isIpInCidr('127.255.255.255', '0.0.0.0/1')).toBe(true);
749
+ expect(isIpInCidr('128.0.0.0', '0.0.0.0/1')).toBe(false);
750
+ });
751
+
752
+ it('IPv6 :: and 0:0:0:0:0:0:0:1 are equivalent', () => {
753
+ expect(isIpInCidr('0:0:0:0:0:0:0:1', '::1/128')).toBe(true);
754
+ expect(isIpInCidr('::1', '0:0:0:0:0:0:0:1/128')).toBe(true);
755
+ });
756
+
757
+ it('IPv4 octal prevention: 010 is not 10', () => {
758
+ // parseIPv4 uses String(n) comparison to reject leading zeros
759
+ expect(isIpInCidr('010.0.0.1', '10.0.0.0/8')).toBe(false);
760
+ });
761
+
762
+ it('IPv6 /64 remainBits=0 scenario (exact byte boundary)', () => {
763
+ // /64 means 8 full bytes, 0 remain bits
764
+ expect(isIpInCidr('2001:0db8:0000:0000:ffff:ffff:ffff:ffff', '2001:0db8::/64')).toBe(true);
765
+ expect(isIpInCidr('2001:0db8:0000:0001:0000:0000:0000:0000', '2001:0db8::/64')).toBe(false);
766
+ });
767
+
768
+ it('IPv6 /65 remainBits=1 scenario (1 bit past byte boundary)', () => {
769
+ // /65 means 8 full bytes, 1 remain bit in byte[8]
770
+ expect(isIpInCidr('2001:db8::7fff:ffff:ffff:ffff', '2001:db8::/65')).toBe(true);
771
+ expect(isIpInCidr('2001:db8::8000:0:0:0', '2001:db8::/65')).toBe(false);
772
+ });
773
+ });