@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,711 @@
1
+ /**
2
+ * 서버 단위 테스트 — Backup Restore endpoints (skipWipe)
3
+ * backup-restore.test.ts
4
+ *
5
+ * 실행: cd packages/server && npx vitest run src/__tests__/backup-restore.test.ts
6
+ *
7
+ * 테스트 대상:
8
+ * A. restore-d1: skipWipe=false (wipe + insert), skipWipe=true (insert only)
9
+ * B. restore-data: skipWipe=false (wipe + insert), skipWipe=true (insert only), D1 path
10
+ * C. cleanup-plugin: namespaced plugin table cleanup
11
+ * D. Edge cases: empty tables, missing namespace
12
+ */
13
+
14
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
15
+ import { backupRoute } from '../routes/backup.js';
16
+ import { setConfig } from '../lib/do-router.js';
17
+ import { resetSchemaInit } from '../lib/auth-d1.js';
18
+ import { resetControlSchemaInit } from '../lib/control-db.js';
19
+ import { _resetD1SchemaCache } from '../lib/d1-schema-init.js';
20
+ import { OpenAPIHono, type HonoEnv } from '../lib/hono.js';
21
+
22
+ // ─── Mock D1 Database ─────────────────────────────────────────────────────────
23
+
24
+ interface BatchCall {
25
+ stmts: Array<{ sql: string; params?: unknown[] }>;
26
+ }
27
+
28
+ interface DirectCall {
29
+ sql: string;
30
+ params?: unknown[];
31
+ }
32
+
33
+ function createMockD1(options?: {
34
+ allResults?: Array<{ match: RegExp | string; results: unknown[] }>;
35
+ }): D1Database & {
36
+ _batchCalls: BatchCall[];
37
+ _directCalls: DirectCall[];
38
+ _allSql: () => string[];
39
+ _directSql: () => string[];
40
+ } {
41
+ const batchCalls: BatchCall[] = [];
42
+ const directCalls: DirectCall[] = [];
43
+
44
+ function getAllResults(sql: string): unknown[] {
45
+ for (const entry of options?.allResults ?? []) {
46
+ if (
47
+ (typeof entry.match === 'string' && sql.includes(entry.match)) ||
48
+ (entry.match instanceof RegExp && entry.match.test(sql))
49
+ ) {
50
+ return entry.results;
51
+ }
52
+ }
53
+ return [];
54
+ }
55
+
56
+ function makeStmt(sql: string, bindings: unknown[] = []): any {
57
+ const stmtData = { sql, params: bindings };
58
+
59
+ const stmt: any = {
60
+ _sql: sql,
61
+ _params: bindings,
62
+ bind: (...values: unknown[]) => {
63
+ stmtData.params = values;
64
+ return makeStmt(sql, values);
65
+ },
66
+ first: async () => {
67
+ directCalls.push({ sql: stmtData.sql, params: stmtData.params });
68
+ return null;
69
+ },
70
+ all: async () => {
71
+ directCalls.push({ sql: stmtData.sql, params: stmtData.params });
72
+ return { results: getAllResults(stmtData.sql) };
73
+ },
74
+ run: async () => {
75
+ directCalls.push({ sql: stmtData.sql, params: stmtData.params });
76
+ return { success: true };
77
+ },
78
+ };
79
+ // Store a reference so batch can read it
80
+ stmt.__stmtData = stmtData;
81
+ return stmt;
82
+ }
83
+
84
+ const db: any = {
85
+ prepare: (sql: string) => makeStmt(sql),
86
+ batch: async (stmts: any[]) => {
87
+ const call: BatchCall = {
88
+ stmts: stmts.map((s: any) => {
89
+ // Handle both formats: D1PreparedStatement and {sql, params}
90
+ if (s.__stmtData) return s.__stmtData;
91
+ if (s.sql) return { sql: s.sql, params: s.params };
92
+ if (s._sql) return { sql: s._sql, params: s._params };
93
+ return { sql: String(s) };
94
+ }),
95
+ };
96
+ batchCalls.push(call);
97
+ return stmts.map(() => ({ success: true }));
98
+ },
99
+ exec: async () => ({ count: 0, duration: 0 }),
100
+ _batchCalls: batchCalls,
101
+ _directCalls: directCalls,
102
+ _allSql: () => batchCalls.flatMap((c) => c.stmts.map((s) => s.sql)),
103
+ _directSql: () => directCalls.map((call) => call.sql),
104
+ };
105
+
106
+ Object.defineProperty(db, '_batchCalls', { get: () => batchCalls });
107
+ Object.defineProperty(db, '_directCalls', { get: () => directCalls });
108
+ return db;
109
+ }
110
+
111
+ // ─── Test App Factory ────────────────────────────────────────────────────────
112
+
113
+ function createTestApp(
114
+ authDb: D1Database,
115
+ dataDb?: D1Database,
116
+ runtimeConfig?: Record<string, unknown>,
117
+ controlDb?: D1Database,
118
+ ) {
119
+ const baseConfig = {
120
+ serviceKeys: {
121
+ keys: [
122
+ {
123
+ kid: 'root',
124
+ tier: 'root',
125
+ scopes: ['*'],
126
+ secretSource: 'dashboard',
127
+ secretRef: 'SERVICE_KEY',
128
+ },
129
+ ],
130
+ },
131
+ databases: {
132
+ shared: {
133
+ provider: 'd1',
134
+ tables: {
135
+ posts: { schema: { title: { type: 'string' }, content: { type: 'text' } } },
136
+ categories: { schema: { name: { type: 'string' } } },
137
+ },
138
+ },
139
+ },
140
+ } satisfies Record<string, unknown>;
141
+
142
+ setConfig({
143
+ ...baseConfig,
144
+ ...(runtimeConfig ?? {}),
145
+ } as any);
146
+
147
+ // Create a wrapper Hono app with the backup route
148
+ // Skip service key middleware by wrapping the route
149
+ const app = new OpenAPIHono<HonoEnv>();
150
+
151
+ // Mount backup route under /admin/api/backup
152
+ // We need to bypass the service key middleware, so we'll create our own app
153
+ // that sets up env correctly
154
+ app.route('/admin/api/backup', backupRoute);
155
+
156
+ // The env needs AUTH_DB and DB_D1_SHARED bindings, plus SERVICE_KEY
157
+ const env: Record<string, unknown> = {
158
+ AUTH_DB: authDb,
159
+ CONTROL_DB: controlDb ?? authDb,
160
+ DB_D1_SHARED: dataDb ?? authDb,
161
+ SERVICE_KEY: 'test-service-key',
162
+ };
163
+
164
+ return { app, env };
165
+ }
166
+
167
+ async function postRestore(
168
+ app: {
169
+ request: (
170
+ path: string,
171
+ init?: RequestInit,
172
+ env?: Record<string, unknown>,
173
+ ) => Response | Promise<Response>;
174
+ },
175
+ env: Record<string, unknown>,
176
+ path: string,
177
+ body: unknown,
178
+ ): Promise<{ status: number; json: Record<string, unknown> }> {
179
+ const resp = await app.request(
180
+ `/admin/api/backup${path}`,
181
+ {
182
+ method: 'POST',
183
+ headers: {
184
+ 'Content-Type': 'application/json',
185
+ 'X-EdgeBase-Service-Key': 'test-service-key',
186
+ },
187
+ body: JSON.stringify(body),
188
+ },
189
+ env,
190
+ );
191
+
192
+ let json: any;
193
+ try {
194
+ json = await resp.json();
195
+ } catch {
196
+ json = {};
197
+ }
198
+ return { status: resp.status, json };
199
+ }
200
+
201
+ // ─── Tests ───────────────────────────────────────────────────────────────────
202
+
203
+ describe('A. restore-d1 — skipWipe behavior', () => {
204
+ let authDb: ReturnType<typeof createMockD1>;
205
+
206
+ beforeEach(() => {
207
+ authDb = createMockD1();
208
+ resetSchemaInit();
209
+ resetControlSchemaInit();
210
+ _resetD1SchemaCache();
211
+ });
212
+
213
+ afterEach(() => {
214
+ setConfig({});
215
+ });
216
+
217
+ it('skipWipe=false (default) — wipes all auth tables then inserts', async () => {
218
+ const { app, env } = createTestApp(authDb);
219
+ const { status, json } = await postRestore(app, env, '/restore-d1', {
220
+ tables: { _users: [{ id: 'u1', email: 'a@b.com' }] },
221
+ });
222
+
223
+ expect(status).toBe(200);
224
+ expect(json.ok).toBe(true);
225
+
226
+ const allSql = authDb._allSql();
227
+
228
+ // Should contain DELETE statements (wipe)
229
+ const deleteStmts = allSql.filter((s) => s.startsWith('DELETE FROM'));
230
+ expect(deleteStmts.length).toBeGreaterThan(0);
231
+ expect(deleteStmts.some((s) => s.includes('_users'))).toBe(true);
232
+ expect(deleteStmts.some((s) => s.includes('_sessions'))).toBe(true);
233
+
234
+ // Should contain INSERT statement for _users
235
+ const insertStmts = allSql.filter((s) => s.includes('INSERT OR REPLACE'));
236
+ expect(insertStmts.length).toBe(1);
237
+ expect(insertStmts[0]).toContain('_users');
238
+ });
239
+
240
+ it('skipWipe=true — no DELETE, inserts only', async () => {
241
+ const { app, env } = createTestApp(authDb);
242
+ const { status, json } = await postRestore(app, env, '/restore-d1', {
243
+ tables: { _users: [{ id: 'u1', email: 'a@b.com' }] },
244
+ skipWipe: true,
245
+ });
246
+
247
+ expect(status).toBe(200);
248
+ expect(json.ok).toBe(true);
249
+
250
+ const allSql = authDb._allSql();
251
+
252
+ // Should NOT contain any DELETE statements
253
+ const deleteStmts = allSql.filter((s) => s.startsWith('DELETE FROM'));
254
+ expect(deleteStmts.length).toBe(0);
255
+
256
+ // Should contain INSERT statement
257
+ const insertStmts = allSql.filter((s) => s.includes('INSERT OR REPLACE'));
258
+ expect(insertStmts.length).toBe(1);
259
+ expect(insertStmts[0]).toContain('_users');
260
+ });
261
+
262
+ it('skipWipe=false with empty tables — wipes only, no inserts', async () => {
263
+ const { app, env } = createTestApp(authDb);
264
+ const { status, json } = await postRestore(app, env, '/restore-d1', {
265
+ tables: {},
266
+ skipWipe: false,
267
+ });
268
+
269
+ expect(status).toBe(200);
270
+ expect(json.ok).toBe(true);
271
+ expect(json.restored).toBe(0);
272
+
273
+ const allSql = authDb._allSql();
274
+
275
+ // Should have DELETE statements (wipe step executed)
276
+ const deleteStmts = allSql.filter((s) => s.startsWith('DELETE FROM'));
277
+ expect(deleteStmts.length).toBeGreaterThan(0);
278
+
279
+ // Should NOT have any INSERT statements (no data to restore)
280
+ const insertStmts = allSql.filter((s) => s.includes('INSERT'));
281
+ expect(insertStmts.length).toBe(0);
282
+ });
283
+
284
+ it('skipWipe=true with empty tables — no operations', async () => {
285
+ const { app, env } = createTestApp(authDb);
286
+ const { status, json } = await postRestore(app, env, '/restore-d1', {
287
+ tables: {},
288
+ skipWipe: true,
289
+ });
290
+
291
+ expect(status).toBe(200);
292
+ expect(json.restored).toBe(0);
293
+
294
+ const allSql = authDb._allSql();
295
+ // No deletes, no inserts — just schema init
296
+ const deleteStmts = allSql.filter((s) => s.startsWith('DELETE FROM'));
297
+ const insertStmts = allSql.filter((s) => s.includes('INSERT OR REPLACE INTO "_'));
298
+ expect(deleteStmts.length).toBe(0);
299
+ expect(insertStmts.length).toBe(0);
300
+ });
301
+
302
+ it('multiple per-table restores with skipWipe=true accumulate data', async () => {
303
+ const { app, env } = createTestApp(authDb);
304
+
305
+ // Simulate: wipe first, then restore per-table
306
+ await postRestore(app, env, '/restore-d1', { tables: {}, skipWipe: false });
307
+ const wipeSql = authDb._allSql();
308
+ const wipeDeletes = wipeSql.filter((s) => s.startsWith('DELETE FROM'));
309
+ expect(wipeDeletes.length).toBeGreaterThan(0);
310
+
311
+ // Now restore table 1
312
+ const r1 = await postRestore(app, env, '/restore-d1', {
313
+ tables: { _users: [{ id: 'u1', email: 'a@b.com' }] },
314
+ skipWipe: true,
315
+ });
316
+ expect(r1.json.ok).toBe(true);
317
+ expect(r1.json.restored).toBe(1);
318
+
319
+ // Restore table 2
320
+ const r2 = await postRestore(app, env, '/restore-d1', {
321
+ tables: { _sessions: [{ id: 's1', userId: 'u1', token: 'abc' }] },
322
+ skipWipe: true,
323
+ });
324
+ expect(r2.json.ok).toBe(true);
325
+ expect(r2.json.restored).toBe(1);
326
+
327
+ // Each per-table call should NOT contain any deletes
328
+ // Total batch calls: schema init + wipe + 2x insert
329
+ const allSql = authDb._allSql();
330
+ // Count inserts after the wipe batch call
331
+ const insertAfterWipe = allSql.filter((s) => s.includes('INSERT OR REPLACE'));
332
+ expect(insertAfterWipe.length).toBe(2); // _users + _sessions
333
+ });
334
+ });
335
+
336
+ // ─── B. restore-data — skipWipe behavior (D1 path) ───
337
+
338
+ describe('B. restore-data — skipWipe behavior (D1 path)', () => {
339
+ let dataDb: ReturnType<typeof createMockD1>;
340
+ let authDb: ReturnType<typeof createMockD1>;
341
+
342
+ beforeEach(() => {
343
+ dataDb = createMockD1();
344
+ authDb = createMockD1();
345
+ resetSchemaInit();
346
+ resetControlSchemaInit();
347
+ _resetD1SchemaCache();
348
+ });
349
+
350
+ afterEach(() => {
351
+ setConfig({});
352
+ });
353
+
354
+ it('skipWipe=false — wipes data tables then inserts', async () => {
355
+ const { app, env } = createTestApp(authDb, dataDb);
356
+ const { status, json } = await postRestore(app, env, '/restore-data', {
357
+ namespace: 'shared',
358
+ tables: { posts: [{ id: 'p1', title: 'Hello' }] },
359
+ });
360
+
361
+ expect(status).toBe(200);
362
+ expect(json.ok).toBe(true);
363
+
364
+ const allSql = dataDb._allSql();
365
+
366
+ // Should have DELETE for user tables + _meta
367
+ const deleteStmts = allSql.filter((s) => s.startsWith('DELETE FROM'));
368
+ expect(deleteStmts.length).toBeGreaterThan(0);
369
+ expect(deleteStmts.some((s) => s.includes('posts'))).toBe(true);
370
+
371
+ // Should have INSERT for posts
372
+ const insertStmts = allSql.filter((s) => s.includes('INSERT OR REPLACE'));
373
+ expect(insertStmts.length).toBe(1);
374
+ expect(insertStmts[0]).toContain('posts');
375
+ });
376
+
377
+ it('skipWipe=true — inserts only, no DELETE', async () => {
378
+ const { app, env } = createTestApp(authDb, dataDb);
379
+ const { status, json } = await postRestore(app, env, '/restore-data', {
380
+ namespace: 'shared',
381
+ tables: { posts: [{ id: 'p1', title: 'Hello' }] },
382
+ skipWipe: true,
383
+ });
384
+
385
+ expect(status).toBe(200);
386
+ expect(json.ok).toBe(true);
387
+
388
+ const allSql = dataDb._allSql();
389
+
390
+ // No deletes
391
+ const deleteStmts = allSql.filter((s) => s.startsWith('DELETE FROM'));
392
+ expect(deleteStmts.length).toBe(0);
393
+
394
+ // Has inserts
395
+ const insertStmts = allSql.filter((s) => s.includes('INSERT OR REPLACE'));
396
+ expect(insertStmts.length).toBe(1);
397
+ });
398
+
399
+ it('wipe-only call (empty tables, skipWipe=false)', async () => {
400
+ const { app, env } = createTestApp(authDb, dataDb);
401
+ const { status, json } = await postRestore(app, env, '/restore-data', {
402
+ namespace: 'shared',
403
+ tables: {},
404
+ skipWipe: false,
405
+ });
406
+
407
+ expect(status).toBe(200);
408
+ expect(json.restored).toBe(0);
409
+
410
+ const allSql = dataDb._allSql();
411
+ const deleteStmts = allSql.filter((s) => s.startsWith('DELETE FROM'));
412
+ expect(deleteStmts.length).toBeGreaterThan(0);
413
+
414
+ const insertStmts = allSql.filter((s) => s.includes('INSERT'));
415
+ expect(insertStmts.length).toBe(0);
416
+ });
417
+
418
+ it('unknown namespace returns 404', async () => {
419
+ const { app, env } = createTestApp(authDb, dataDb);
420
+ const { status, json } = await postRestore(app, env, '/restore-data', {
421
+ namespace: 'nonexistent',
422
+ tables: {},
423
+ });
424
+
425
+ expect(status).toBe(404);
426
+ expect(json.message).toContain('nonexistent');
427
+ });
428
+
429
+ it('per-table restore flow: wipe then restore each table separately', async () => {
430
+ const { app, env } = createTestApp(authDb, dataDb);
431
+
432
+ // Step 1: Wipe
433
+ const wipeResult = await postRestore(app, env, '/restore-data', {
434
+ namespace: 'shared',
435
+ tables: {},
436
+ skipWipe: false,
437
+ });
438
+ expect(wipeResult.status).toBe(200);
439
+
440
+ // Step 2: Restore posts
441
+ const r1 = await postRestore(app, env, '/restore-data', {
442
+ namespace: 'shared',
443
+ tables: {
444
+ posts: [
445
+ { id: 'p1', title: 'A' },
446
+ { id: 'p2', title: 'B' },
447
+ ],
448
+ },
449
+ skipWipe: true,
450
+ });
451
+ expect(r1.json.ok).toBe(true);
452
+ expect(r1.json.restored).toBe(1);
453
+
454
+ // Step 3: Restore _meta
455
+ const r2 = await postRestore(app, env, '/restore-data', {
456
+ namespace: 'shared',
457
+ tables: { _meta: [{ key: 'k1', value: 'v1' }] },
458
+ skipWipe: true,
459
+ });
460
+ expect(r2.json.ok).toBe(true);
461
+ expect(r2.json.restored).toBe(1);
462
+
463
+ const allSql = dataDb._allSql();
464
+ const inserts = allSql.filter((s) => s.includes('INSERT OR REPLACE'));
465
+ // 2 posts + 1 _meta = 3 inserts total
466
+ expect(inserts.length).toBe(3);
467
+ });
468
+ });
469
+
470
+ // ─── C. cleanup-plugin ───
471
+
472
+ describe('C. cleanup-plugin', () => {
473
+ beforeEach(() => {
474
+ resetSchemaInit();
475
+ resetControlSchemaInit();
476
+ _resetD1SchemaCache();
477
+ });
478
+
479
+ afterEach(() => {
480
+ setConfig({});
481
+ });
482
+
483
+ it('restores control-plane metadata in CONTROL_DB', async () => {
484
+ const authDb = createMockD1();
485
+ const controlDb = createMockD1();
486
+ const { app, env } = createTestApp(authDb, undefined, undefined, controlDb);
487
+
488
+ const { status, json } = await postRestore(app, env, '/restore-control-d1', {
489
+ tables: {
490
+ _meta: [{ key: 'plugin_version:plugin-a', value: '1.2.0' }],
491
+ },
492
+ });
493
+
494
+ expect(status).toBe(200);
495
+ expect(json.ok).toBe(true);
496
+
497
+ const allSql = controlDb._allSql();
498
+ expect(allSql.some((sql) => sql === 'DELETE FROM "_meta"')).toBe(true);
499
+ expect(allSql.some((sql) => sql.includes('INSERT OR REPLACE INTO "_meta"'))).toBe(true);
500
+ });
501
+
502
+ it('removes only plugin-prefixed D1 tables and metadata', async () => {
503
+ const authDb = createMockD1();
504
+ const controlDb = createMockD1();
505
+ const dataDb = createMockD1({
506
+ allResults: [
507
+ {
508
+ match: /sqlite_master/,
509
+ results: [
510
+ { name: 'posts' },
511
+ { name: 'plugin-a/events' },
512
+ { name: 'plugin-a/events_fts' },
513
+ { name: 'plugin-a/events_fts_data' },
514
+ { name: 'plugin-a/logs' },
515
+ ],
516
+ },
517
+ ],
518
+ });
519
+
520
+ const { app, env } = createTestApp(
521
+ authDb,
522
+ dataDb,
523
+ {
524
+ databases: {
525
+ shared: {
526
+ provider: 'd1',
527
+ tables: {
528
+ posts: { schema: { title: { type: 'string' } } },
529
+ },
530
+ },
531
+ },
532
+ },
533
+ controlDb,
534
+ );
535
+
536
+ const { status, json } = await postRestore(app, env, '/cleanup-plugin', {
537
+ prefix: 'plugin-a',
538
+ namespace: 'shared',
539
+ });
540
+
541
+ expect(status).toBe(200);
542
+ expect(json.ok).toBe(true);
543
+ expect(json.removed).toEqual({
544
+ tables: ['plugin-a/events', 'plugin-a/logs'],
545
+ metaKeys: [
546
+ 'schemaHash:plugin-a/events',
547
+ 'migration_version:plugin-a/events',
548
+ 'schemaHash:plugin-a/logs',
549
+ 'migration_version:plugin-a/logs',
550
+ 'plugin_version:plugin-a',
551
+ ],
552
+ });
553
+
554
+ const directSql = dataDb._directSql();
555
+ expect(directSql).toContain('DROP TABLE IF EXISTS "plugin-a/events_fts"');
556
+ expect(directSql).toContain('DROP TABLE IF EXISTS "plugin-a/events"');
557
+ expect(directSql).toContain('DROP TABLE IF EXISTS "plugin-a/logs_fts"');
558
+ expect(directSql).toContain('DROP TABLE IF EXISTS "plugin-a/logs"');
559
+ expect(directSql).not.toContain('DROP TABLE IF EXISTS "plugin-a/events_fts_fts"');
560
+ expect(directSql).not.toContain('DROP TABLE IF EXISTS "posts_fts"');
561
+
562
+ const metaDeletes = dataDb._directCalls
563
+ .filter((call) => call.sql === 'DELETE FROM "_meta" WHERE "key" = ?')
564
+ .map((call) => call.params?.[0]);
565
+ expect(metaDeletes).toEqual([
566
+ 'schemaHash:plugin-a/events',
567
+ 'migration_version:plugin-a/events',
568
+ 'schemaHash:plugin-a/logs',
569
+ 'migration_version:plugin-a/logs',
570
+ ]);
571
+
572
+ const controlDeletes = controlDb._directCalls
573
+ .filter((call) => call.sql === 'DELETE FROM _meta WHERE key = ?')
574
+ .map((call) => call.params?.[0]);
575
+ expect(controlDeletes).toContain('plugin_version:plugin-a');
576
+ });
577
+ });
578
+
579
+ // ─── D. Edge cases ───
580
+
581
+ describe('D. restore edge cases', () => {
582
+ beforeEach(() => {
583
+ resetSchemaInit();
584
+ resetControlSchemaInit();
585
+ _resetD1SchemaCache();
586
+ });
587
+
588
+ afterEach(() => {
589
+ setConfig({});
590
+ });
591
+
592
+ it('restore-d1 without tables field returns 400', async () => {
593
+ const authDb = createMockD1();
594
+ const { app, env } = createTestApp(authDb);
595
+ const { status } = await postRestore(app, env, '/restore-d1', {} as any);
596
+
597
+ expect(status).toBe(400);
598
+ });
599
+
600
+ it('restore-data without namespace returns 400', async () => {
601
+ const authDb = createMockD1();
602
+ const { app, env } = createTestApp(authDb);
603
+ const { status } = await postRestore(app, env, '/restore-data', {
604
+ tables: {},
605
+ } as any);
606
+
607
+ expect(status).toBe(400);
608
+ });
609
+
610
+ it('service key missing returns 403', async () => {
611
+ const authDb = createMockD1();
612
+ const { app, env } = createTestApp(authDb);
613
+
614
+ const resp = await app.request(
615
+ '/admin/api/backup/restore-d1',
616
+ {
617
+ method: 'POST',
618
+ headers: { 'Content-Type': 'application/json' },
619
+ body: JSON.stringify({ tables: {} }),
620
+ },
621
+ env,
622
+ );
623
+
624
+ expect(resp.status).toBe(403);
625
+ });
626
+
627
+ it('invalid service key returns 401', async () => {
628
+ const authDb = createMockD1();
629
+ const { app, env } = createTestApp(authDb);
630
+
631
+ const resp = await app.request(
632
+ '/admin/api/backup/restore-d1',
633
+ {
634
+ method: 'POST',
635
+ headers: {
636
+ 'Content-Type': 'application/json',
637
+ 'X-EdgeBase-Service-Key': 'wrong-key',
638
+ },
639
+ body: JSON.stringify({ tables: {} }),
640
+ },
641
+ env,
642
+ );
643
+
644
+ expect(resp.status).toBe(401);
645
+ });
646
+ });
647
+
648
+ describe('E. export routes', () => {
649
+ beforeEach(() => {
650
+ resetSchemaInit();
651
+ resetControlSchemaInit();
652
+ _resetD1SchemaCache();
653
+ });
654
+
655
+ afterEach(() => {
656
+ setConfig({});
657
+ });
658
+
659
+ it('backup export reads shared D1 tables instead of assuming Durable Objects', async () => {
660
+ const authDb = createMockD1();
661
+ const dataDb = createMockD1({
662
+ allResults: [
663
+ {
664
+ match: 'SELECT * FROM "posts"',
665
+ results: [{ id: 'post_1', title: 'Hello export' }],
666
+ },
667
+ {
668
+ match: 'SELECT * FROM "_meta"',
669
+ results: [],
670
+ },
671
+ ],
672
+ });
673
+ const { app, env } = createTestApp(authDb, dataDb);
674
+
675
+ const resp = await app.request(
676
+ '/admin/api/backup/export/posts?format=json',
677
+ {
678
+ method: 'GET',
679
+ headers: {
680
+ 'X-EdgeBase-Service-Key': 'test-service-key',
681
+ },
682
+ },
683
+ env,
684
+ );
685
+
686
+ expect(resp.status).toBe(200);
687
+ await expect(resp.json()).resolves.toEqual([{ id: 'post_1', title: 'Hello export' }]);
688
+ expect(dataDb._directSql()).toContain('SELECT * FROM "posts"');
689
+ });
690
+
691
+ it('list-dos accepts an empty JSON body for config-scan backups', async () => {
692
+ const authDb = createMockD1();
693
+ const { app, env } = createTestApp(authDb);
694
+
695
+ const resp = await app.request(
696
+ '/admin/api/backup/list-dos',
697
+ {
698
+ method: 'POST',
699
+ headers: {
700
+ 'Content-Type': 'application/json',
701
+ 'X-EdgeBase-Service-Key': 'test-service-key',
702
+ },
703
+ body: '{}',
704
+ },
705
+ env,
706
+ );
707
+
708
+ expect(resp.status).toBe(200);
709
+ await expect(resp.json()).resolves.toMatchObject({ dos: expect.any(Array), total: expect.any(Number) });
710
+ });
711
+ });