@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.
- package/admin-build/.gitkeep +0 -0
- package/admin-build/_app/env.js +1 -0
- package/admin-build/_app/immutable/assets/0.Bm6cF078.css +1 -0
- package/admin-build/_app/immutable/assets/1.BfW3pUNa.css +1 -0
- package/admin-build/_app/immutable/assets/11.CVmQOewb.css +1 -0
- package/admin-build/_app/immutable/assets/12.B1EhbRZT.css +1 -0
- package/admin-build/_app/immutable/assets/13.BvwYeuwE.css +1 -0
- package/admin-build/_app/immutable/assets/14.CdVfcO0R.css +1 -0
- package/admin-build/_app/immutable/assets/15.2yeZ66b-.css +1 -0
- package/admin-build/_app/immutable/assets/17.BVg0JEVu.css +1 -0
- package/admin-build/_app/immutable/assets/18.Rwnl3x_i.css +1 -0
- package/admin-build/_app/immutable/assets/20.DsPWA9AV.css +1 -0
- package/admin-build/_app/immutable/assets/21.Dz2RJ56c.css +1 -0
- package/admin-build/_app/immutable/assets/22.DwNLk5Ai.css +1 -0
- package/admin-build/_app/immutable/assets/23.CFpu0gOO.css +1 -0
- package/admin-build/_app/immutable/assets/24.Cy5LBeoJ.css +1 -0
- package/admin-build/_app/immutable/assets/25.pUyLVf-h.css +1 -0
- package/admin-build/_app/immutable/assets/26.DBcGrlXa.css +1 -0
- package/admin-build/_app/immutable/assets/27.BswYyAJD.css +1 -0
- package/admin-build/_app/immutable/assets/28.B4ueB1Kf.css +1 -0
- package/admin-build/_app/immutable/assets/29.B-qU6PdF.css +1 -0
- package/admin-build/_app/immutable/assets/3.Dg81Pgmd.css +1 -0
- package/admin-build/_app/immutable/assets/30.CsdWum94.css +1 -0
- package/admin-build/_app/immutable/assets/31.U6OwIp50.css +1 -0
- package/admin-build/_app/immutable/assets/4.CyawCCux.css +1 -0
- package/admin-build/_app/immutable/assets/5.C0YO2HTk.css +1 -0
- package/admin-build/_app/immutable/assets/8.Br5jd6kD.css +1 -0
- package/admin-build/_app/immutable/assets/Badge.EMYLHBxE.css +1 -0
- package/admin-build/_app/immutable/assets/Button.DpzMRTjK.css +1 -0
- package/admin-build/_app/immutable/assets/ConfirmDialog.DAnaWRRk.css +1 -0
- package/admin-build/_app/immutable/assets/EmptyState.CwKsu57Y.css +1 -0
- package/admin-build/_app/immutable/assets/Input.BDUSenmU.css +1 -0
- package/admin-build/_app/immutable/assets/Modal.Dm5B0Xie.css +1 -0
- package/admin-build/_app/immutable/assets/PageShell.CmU-Xh-b.css +1 -0
- package/admin-build/_app/immutable/assets/SchemaFieldEditor.g4NsCdno.css +1 -0
- package/admin-build/_app/immutable/assets/Select.BW4Keufm.css +1 -0
- package/admin-build/_app/immutable/assets/Skeleton.KWUulTKJ.css +1 -0
- package/admin-build/_app/immutable/assets/Tabs.CniGYb67.css +1 -0
- package/admin-build/_app/immutable/assets/TimeChart.BTCDAvmT.css +1 -0
- package/admin-build/_app/immutable/assets/Toggle.Cy_K12OM.css +1 -0
- package/admin-build/_app/immutable/assets/TopList.ClFzmPlA.css +1 -0
- package/admin-build/_app/immutable/chunks/7B47DvSx.js +1 -0
- package/admin-build/_app/immutable/chunks/7f08Id8e.js +1 -0
- package/admin-build/_app/immutable/chunks/8wJeQ7LN.js +1 -0
- package/admin-build/_app/immutable/chunks/B-h2afW5.js +1 -0
- package/admin-build/_app/immutable/chunks/B8vJP3wz.js +1 -0
- package/admin-build/_app/immutable/chunks/BR_fL5Yv.js +1 -0
- package/admin-build/_app/immutable/chunks/BY92tFS2.js +1 -0
- package/admin-build/_app/immutable/chunks/BcR-Rdj9.js +1 -0
- package/admin-build/_app/immutable/chunks/BdrwyZv8.js +1 -0
- package/admin-build/_app/immutable/chunks/Bh56EfQ_.js +1 -0
- package/admin-build/_app/immutable/chunks/BkrCkgYp.js +1 -0
- package/admin-build/_app/immutable/chunks/BmRjiP5k.js +1 -0
- package/admin-build/_app/immutable/chunks/BsokvhWC.js +1 -0
- package/admin-build/_app/immutable/chunks/C4D51vTW.js +1 -0
- package/admin-build/_app/immutable/chunks/C6puvcoR.js +2 -0
- package/admin-build/_app/immutable/chunks/CCKNu7m7.js +1 -0
- package/admin-build/_app/immutable/chunks/CWj6FrbW.js +1 -0
- package/admin-build/_app/immutable/chunks/Ce-ngf4p.js +5 -0
- package/admin-build/_app/immutable/chunks/Cs0GwzJA.js +1 -0
- package/admin-build/_app/immutable/chunks/CwROoZK0.js +1 -0
- package/admin-build/_app/immutable/chunks/CxCPv_Ut.js +1 -0
- package/admin-build/_app/immutable/chunks/CxbRue-5.js +1 -0
- package/admin-build/_app/immutable/chunks/CyqB6g-D.js +1 -0
- package/admin-build/_app/immutable/chunks/D5h5A1cc.js +2 -0
- package/admin-build/_app/immutable/chunks/DnyL7Zq-.js +1 -0
- package/admin-build/_app/immutable/chunks/DoPXzH7F.js +1 -0
- package/admin-build/_app/immutable/chunks/DrQSgw-f.js +1 -0
- package/admin-build/_app/immutable/chunks/DttM2zNO.js +1 -0
- package/admin-build/_app/immutable/chunks/DuXuUBWN.js +1 -0
- package/admin-build/_app/immutable/chunks/MdeqaOQx.js +10 -0
- package/admin-build/_app/immutable/chunks/NuUjtcO2.js +1 -0
- package/admin-build/_app/immutable/chunks/Q2nPFxS6.js +1 -0
- package/admin-build/_app/immutable/chunks/R6arueIl.js +1 -0
- package/admin-build/_app/immutable/chunks/UUazaC_N.js +1 -0
- package/admin-build/_app/immutable/chunks/cOYbrQxx.js +1 -0
- package/admin-build/_app/immutable/chunks/eFQHTGwA.js +1 -0
- package/admin-build/_app/immutable/chunks/ehbppgYb.js +1 -0
- package/admin-build/_app/immutable/chunks/glwixJlP.js +1 -0
- package/admin-build/_app/immutable/chunks/vApWTCBs.js +1 -0
- package/admin-build/_app/immutable/chunks/w89G9Xpi.js +1 -0
- package/admin-build/_app/immutable/chunks/wJsUhbfZ.js +1 -0
- package/admin-build/_app/immutable/chunks/zfauFM8P.js +1 -0
- package/admin-build/_app/immutable/entry/app.CcO-Uos3.js +2 -0
- package/admin-build/_app/immutable/entry/start.COebYq3I.js +1 -0
- package/admin-build/_app/immutable/nodes/0.CjtHKU-6.js +1 -0
- package/admin-build/_app/immutable/nodes/1.DEisjlM0.js +1 -0
- package/admin-build/_app/immutable/nodes/10.CvhdyWVB.js +1 -0
- package/admin-build/_app/immutable/nodes/11.DjHqcOvy.js +1 -0
- package/admin-build/_app/immutable/nodes/12.mQLz4Mj_.js +1 -0
- package/admin-build/_app/immutable/nodes/13.CBonZZyP.js +110 -0
- package/admin-build/_app/immutable/nodes/14.d-oiZL0j.js +3 -0
- package/admin-build/_app/immutable/nodes/15.CKPQsUYF.js +1 -0
- package/admin-build/_app/immutable/nodes/16.wPzAPQGx.js +1 -0
- package/admin-build/_app/immutable/nodes/17.DayhKyEZ.js +1 -0
- package/admin-build/_app/immutable/nodes/18.DKwS0Ir0.js +1 -0
- package/admin-build/_app/immutable/nodes/19.wPzAPQGx.js +1 -0
- package/admin-build/_app/immutable/nodes/2.BKoKrw1i.js +1 -0
- package/admin-build/_app/immutable/nodes/20.BvIkkkrW.js +1 -0
- package/admin-build/_app/immutable/nodes/21.DMaFhdHk.js +128 -0
- package/admin-build/_app/immutable/nodes/22.3xdgwuK1.js +1 -0
- package/admin-build/_app/immutable/nodes/23.8Bvgjbsl.js +112 -0
- package/admin-build/_app/immutable/nodes/24.DzSSzRhG.js +2 -0
- package/admin-build/_app/immutable/nodes/25.9KKYBnAE.js +2 -0
- package/admin-build/_app/immutable/nodes/26.Bhn9dfhY.js +1 -0
- package/admin-build/_app/immutable/nodes/27.kRLiC24G.js +1 -0
- package/admin-build/_app/immutable/nodes/28.BVIN1-7N.js +1 -0
- package/admin-build/_app/immutable/nodes/29.3yabZWj4.js +1 -0
- package/admin-build/_app/immutable/nodes/3.BFtSOkX7.js +2 -0
- package/admin-build/_app/immutable/nodes/30.CyCQlwaP.js +1 -0
- package/admin-build/_app/immutable/nodes/31.C4LDXjES.js +1 -0
- package/admin-build/_app/immutable/nodes/4.CvbiMlCa.js +1 -0
- package/admin-build/_app/immutable/nodes/5.C6BLv2eM.js +1 -0
- package/admin-build/_app/immutable/nodes/6.BcXvfl2P.js +1 -0
- package/admin-build/_app/immutable/nodes/7.CIuqhPiK.js +1 -0
- package/admin-build/_app/immutable/nodes/8.BQOR_JfO.js +1 -0
- package/admin-build/_app/immutable/nodes/9.NZqXQxPy.js +1 -0
- package/admin-build/_app/version.json +1 -0
- package/admin-build/favicon.svg +26 -0
- package/admin-build/index.html +45 -0
- package/openapi.json +19543 -0
- package/package.json +66 -0
- package/src/__tests__/admin-assets.test.ts +55 -0
- package/src/__tests__/admin-data-routes.test.ts +488 -0
- package/src/__tests__/admin-db-target.test.ts +103 -0
- package/src/__tests__/admin-routing.test.ts +31 -0
- package/src/__tests__/admin-user-management.test.ts +311 -0
- package/src/__tests__/analytics-query.test.ts +75 -0
- package/src/__tests__/auth-d1.test.ts +749 -0
- package/src/__tests__/auth-db-adapter.test.ts +73 -0
- package/src/__tests__/auth-jwt.test.ts +440 -0
- package/src/__tests__/auth-oauth.test.ts +389 -0
- package/src/__tests__/auth-password.test.ts +367 -0
- package/src/__tests__/auth-redirect.test.ts +87 -0
- package/src/__tests__/backup-restore.test.ts +711 -0
- package/src/__tests__/broadcast.test.ts +128 -0
- package/src/__tests__/cli.test.ts +178 -0
- package/src/__tests__/cloudflare-realtime.test.ts +113 -0
- package/src/__tests__/config.test.ts +469 -0
- package/src/__tests__/cors.test.ts +154 -0
- package/src/__tests__/cron.test.ts +302 -0
- package/src/__tests__/d1-handler.test.ts +402 -0
- package/src/__tests__/d1-sql.test.ts +120 -0
- package/src/__tests__/database-live-config.test.ts +42 -0
- package/src/__tests__/database-live-emitter.test.ts +56 -0
- package/src/__tests__/database-live-filters.test.ts +63 -0
- package/src/__tests__/database-live-route.test.ts +113 -0
- package/src/__tests__/db-sql.test.ts +163 -0
- package/src/__tests__/do-lifecycle.test.ts +263 -0
- package/src/__tests__/do-router.test.ts +729 -0
- package/src/__tests__/email-provider.test.ts +128 -0
- package/src/__tests__/email-templates.test.ts +528 -0
- package/src/__tests__/error-format.test.ts +250 -0
- package/src/__tests__/field-ops.test.ts +242 -0
- package/src/__tests__/functions-context.test.ts +334 -0
- package/src/__tests__/functions-d1-proxy.test.ts +229 -0
- package/src/__tests__/functions-registry-runtime-config.test.ts +17 -0
- package/src/__tests__/functions-route.test.ts +139 -0
- package/src/__tests__/internal-request.test.ts +77 -0
- package/src/__tests__/log-writer.test.ts +44 -0
- package/src/__tests__/logger.test.ts +58 -0
- package/src/__tests__/meta-admin-proxy.test.ts +48 -0
- package/src/__tests__/meta-export-coverage.test.ts +191 -0
- package/src/__tests__/meta-route-registration.test.ts +47 -0
- package/src/__tests__/namespace-dump.test.ts +28 -0
- package/src/__tests__/oauth-providers.test.ts +337 -0
- package/src/__tests__/openapi-coverage.test.ts +144 -0
- package/src/__tests__/pagination.test.ts +59 -0
- package/src/__tests__/password-policy.test.ts +191 -0
- package/src/__tests__/plugin-migrations.test.ts +379 -0
- package/src/__tests__/postgres-batch-compat.test.ts +133 -0
- package/src/__tests__/postgres-dialect.test.ts +328 -0
- package/src/__tests__/postgres-executor.test.ts +79 -0
- package/src/__tests__/postgres-field-ops-compat.test.ts +222 -0
- package/src/__tests__/postgres-schema-init.test.ts +105 -0
- package/src/__tests__/postgres-table-utils.test.ts +107 -0
- package/src/__tests__/presence.test.ts +199 -0
- package/src/__tests__/provider.test.ts +550 -0
- package/src/__tests__/public-user-profile.test.ts +339 -0
- package/src/__tests__/push-handlers.test.ts +179 -0
- package/src/__tests__/push-provider.test.ts +80 -0
- package/src/__tests__/push-token.test.ts +418 -0
- package/src/__tests__/query.test.ts +771 -0
- package/src/__tests__/rate-limit.test.ts +260 -0
- package/src/__tests__/room-access-policy.test.ts +101 -0
- package/src/__tests__/room-handler-context.test.ts +130 -0
- package/src/__tests__/room-monitoring.test.ts +138 -0
- package/src/__tests__/room-runtime-routing.test.ts +222 -0
- package/src/__tests__/room.test.ts +254 -0
- package/src/__tests__/route-parser.test.ts +490 -0
- package/src/__tests__/rules.test.ts +234 -0
- package/src/__tests__/runtime-surface-accounting.test.ts +120 -0
- package/src/__tests__/scheduled.test.ts +80 -0
- package/src/__tests__/schema.test.ts +1273 -0
- package/src/__tests__/security-hardening.test.ts +312 -0
- package/src/__tests__/server.unit.test.ts +333 -0
- package/src/__tests__/service-key-db-proxy.test.ts +650 -0
- package/src/__tests__/service-key-provider-bypass.test.ts +138 -0
- package/src/__tests__/service-key.test.ts +757 -0
- package/src/__tests__/smoke-skip-report.test.ts +72 -0
- package/src/__tests__/sms-provider.test.ts +39 -0
- package/src/__tests__/sql-route.test.ts +218 -0
- package/src/__tests__/storage-hook-context.test.ts +115 -0
- package/src/__tests__/totp.test.ts +200 -0
- package/src/__tests__/uuid.test.ts +144 -0
- package/src/__tests__/validation.test.ts +773 -0
- package/src/__tests__/websocket-pending.test.ts +163 -0
- package/src/_functions-registry.ts +51 -0
- package/src/bench-entry.ts +9 -0
- package/src/cloudflare-test.d.ts +1 -0
- package/src/durable-objects/auth-do.ts +49 -0
- package/src/durable-objects/database-do.ts +2240 -0
- package/src/durable-objects/database-live-do.ts +949 -0
- package/src/durable-objects/logs-do.ts +1200 -0
- package/src/durable-objects/room-runtime-base.ts +1604 -0
- package/src/durable-objects/rooms-do.ts +2191 -0
- package/src/generated-config.ts +6 -0
- package/src/index.ts +382 -0
- package/src/lib/admin-assets.ts +54 -0
- package/src/lib/admin-db-target.ts +301 -0
- package/src/lib/admin-routing.ts +35 -0
- package/src/lib/admin-user-management.ts +464 -0
- package/src/lib/analytics-adapter.ts +103 -0
- package/src/lib/analytics-query.ts +579 -0
- package/src/lib/auth-d1-service.ts +1193 -0
- package/src/lib/auth-d1.ts +1056 -0
- package/src/lib/auth-db-adapter.ts +289 -0
- package/src/lib/auth-redirect.ts +116 -0
- package/src/lib/cidr.ts +115 -0
- package/src/lib/client-ip.ts +51 -0
- package/src/lib/cloudflare-realtime.ts +251 -0
- package/src/lib/control-db.ts +36 -0
- package/src/lib/cron.ts +163 -0
- package/src/lib/d1-handler.ts +1425 -0
- package/src/lib/d1-schema-init.ts +255 -0
- package/src/lib/d1-sql.ts +33 -0
- package/src/lib/database-live-config.ts +24 -0
- package/src/lib/database-live-emitter.ts +111 -0
- package/src/lib/db-sql.ts +66 -0
- package/src/lib/do-retry.ts +36 -0
- package/src/lib/do-router.ts +270 -0
- package/src/lib/do-sql.ts +73 -0
- package/src/lib/email-provider.ts +379 -0
- package/src/lib/email-templates.ts +285 -0
- package/src/lib/email-translations.ts +422 -0
- package/src/lib/errors.ts +151 -0
- package/src/lib/functions.ts +2091 -0
- package/src/lib/hono.ts +56 -0
- package/src/lib/internal-request.ts +56 -0
- package/src/lib/jwt.ts +354 -0
- package/src/lib/log-writer.ts +272 -0
- package/src/lib/namespace-dump.ts +125 -0
- package/src/lib/oauth-providers.ts +1225 -0
- package/src/lib/op-parser.ts +99 -0
- package/src/lib/openapi.ts +146 -0
- package/src/lib/pagination.ts +19 -0
- package/src/lib/password-policy.ts +102 -0
- package/src/lib/password.ts +145 -0
- package/src/lib/plugin-migrations.ts +612 -0
- package/src/lib/postgres-executor.ts +203 -0
- package/src/lib/postgres-handler.ts +1102 -0
- package/src/lib/postgres-schema-init.ts +341 -0
- package/src/lib/postgres-table-utils.ts +87 -0
- package/src/lib/public-user-profile.ts +187 -0
- package/src/lib/push-provider.ts +409 -0
- package/src/lib/push-token.ts +294 -0
- package/src/lib/query-engine.ts +768 -0
- package/src/lib/room-monitoring.ts +97 -0
- package/src/lib/room-runtime.ts +14 -0
- package/src/lib/route-parser.ts +434 -0
- package/src/lib/schema.ts +538 -0
- package/src/lib/schemas.ts +152 -0
- package/src/lib/service-key.ts +419 -0
- package/src/lib/sms-provider.ts +230 -0
- package/src/lib/startup-config.ts +99 -0
- package/src/lib/totp.ts +242 -0
- package/src/lib/uuid.ts +87 -0
- package/src/lib/validation.ts +205 -0
- package/src/lib/version.ts +2 -0
- package/src/lib/websocket-pending.ts +40 -0
- package/src/middleware/auth.ts +169 -0
- package/src/middleware/captcha-verify.ts +217 -0
- package/src/middleware/cors.ts +159 -0
- package/src/middleware/error-handler.ts +54 -0
- package/src/middleware/internal-guard.ts +26 -0
- package/src/middleware/logger.ts +126 -0
- package/src/middleware/rate-limit.ts +283 -0
- package/src/middleware/rules.ts +475 -0
- package/src/routes/admin-auth.ts +447 -0
- package/src/routes/admin.ts +3501 -0
- package/src/routes/analytics-api.ts +290 -0
- package/src/routes/auth.ts +4222 -0
- package/src/routes/backup.ts +1466 -0
- package/src/routes/config.ts +53 -0
- package/src/routes/d1.ts +109 -0
- package/src/routes/database-live.ts +281 -0
- package/src/routes/functions.ts +155 -0
- package/src/routes/health.ts +32 -0
- package/src/routes/kv.ts +167 -0
- package/src/routes/oauth.ts +1055 -0
- package/src/routes/push.ts +1465 -0
- package/src/routes/room.ts +639 -0
- package/src/routes/schema-endpoint.ts +76 -0
- package/src/routes/sql.ts +176 -0
- package/src/routes/storage.ts +1674 -0
- package/src/routes/tables.ts +699 -0
- package/src/routes/users.ts +21 -0
- package/src/routes/vectorize.ts +372 -0
- package/src/types.ts +99 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 서버 단위 테스트 — lib/auth-d1.ts
|
|
3
|
+
*
|
|
4
|
+
* 실행: cd packages/server && npx vitest run src/__tests__/auth-d1.test.ts
|
|
5
|
+
*
|
|
6
|
+
* 테스트 대상:
|
|
7
|
+
* ensureAuthSchema / resetSchemaInit — 스키마 초기화 idempotency
|
|
8
|
+
* Email index — lookup, register pending, confirm, delete
|
|
9
|
+
* OAuth index — lookup, register pending (duplicate), confirm, delete
|
|
10
|
+
* Phone index — lookup, register pending (duplicate), confirm, delete
|
|
11
|
+
* Anon index — register, confirm, delete, batchDelete
|
|
12
|
+
* Admin CRUD — create, getByEmail, getById, adminExists
|
|
13
|
+
* Admin sessions — create, get, delete
|
|
14
|
+
* User listing — listUserMappings, searchUserMappingsByEmail, countUsers
|
|
15
|
+
* KV token helpers — lookupTokenShard, deleteTokenMapping
|
|
16
|
+
* Passkey index — lookup, register, delete, deleteByUser
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
20
|
+
import {
|
|
21
|
+
ensureAuthSchema,
|
|
22
|
+
resetSchemaInit,
|
|
23
|
+
lookupEmail,
|
|
24
|
+
registerEmailPending,
|
|
25
|
+
confirmEmail,
|
|
26
|
+
deleteEmailPending,
|
|
27
|
+
deleteEmail,
|
|
28
|
+
lookupOAuth,
|
|
29
|
+
registerOAuthPending,
|
|
30
|
+
confirmOAuth,
|
|
31
|
+
deleteOAuth,
|
|
32
|
+
registerAnonPending,
|
|
33
|
+
confirmAnon,
|
|
34
|
+
deleteAnon,
|
|
35
|
+
batchDeleteAnon,
|
|
36
|
+
getAdminByEmail,
|
|
37
|
+
getAdminById,
|
|
38
|
+
adminExists,
|
|
39
|
+
createAdmin,
|
|
40
|
+
getAdminSession,
|
|
41
|
+
createAdminSession,
|
|
42
|
+
deleteAdminSession,
|
|
43
|
+
updateAdminPassword,
|
|
44
|
+
listUserMappings,
|
|
45
|
+
searchUserMappingsByEmail,
|
|
46
|
+
countUsers,
|
|
47
|
+
lookupPhone,
|
|
48
|
+
registerPhonePending,
|
|
49
|
+
confirmPhone,
|
|
50
|
+
deletePhone,
|
|
51
|
+
lookupTokenShard,
|
|
52
|
+
deleteTokenMapping,
|
|
53
|
+
lookupPasskey,
|
|
54
|
+
registerPasskey,
|
|
55
|
+
deletePasskey,
|
|
56
|
+
deletePasskeysByUser,
|
|
57
|
+
} from '../lib/auth-d1.js';
|
|
58
|
+
import type { AuthDb } from '../lib/auth-db-adapter.js';
|
|
59
|
+
|
|
60
|
+
// ─── Mock AuthDb ─────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
interface MockCall {
|
|
63
|
+
sql: string;
|
|
64
|
+
bindings: unknown[];
|
|
65
|
+
method: 'first' | 'query' | 'run';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createMockAuthDb(options: {
|
|
69
|
+
firstResult?: unknown;
|
|
70
|
+
queryResult?: unknown[];
|
|
71
|
+
/** Per-call results: return different values for sequential first() calls */
|
|
72
|
+
firstResults?: unknown[];
|
|
73
|
+
} = {}): AuthDb & { _calls: MockCall[]; _batchCalls: number } {
|
|
74
|
+
const calls: MockCall[] = [];
|
|
75
|
+
let firstCallIdx = 0;
|
|
76
|
+
let batchCalls = 0;
|
|
77
|
+
|
|
78
|
+
const db: any = {
|
|
79
|
+
dialect: 'sqlite' as const,
|
|
80
|
+
|
|
81
|
+
async first(sql: string, params?: unknown[]) {
|
|
82
|
+
const call: MockCall = { sql, bindings: params ?? [], method: 'first' };
|
|
83
|
+
calls.push(call);
|
|
84
|
+
if (options.firstResults && firstCallIdx < options.firstResults.length) {
|
|
85
|
+
return options.firstResults[firstCallIdx++];
|
|
86
|
+
}
|
|
87
|
+
return options.firstResult ?? null;
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
async query(sql: string, params?: unknown[]) {
|
|
91
|
+
const call: MockCall = { sql, bindings: params ?? [], method: 'query' };
|
|
92
|
+
calls.push(call);
|
|
93
|
+
return options.queryResult ?? [];
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
async run(sql: string, params?: unknown[]) {
|
|
97
|
+
const call: MockCall = { sql, bindings: params ?? [], method: 'run' };
|
|
98
|
+
calls.push(call);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async batch(statements: { sql: string; params?: unknown[] }[]) {
|
|
102
|
+
batchCalls++;
|
|
103
|
+
for (const s of statements) {
|
|
104
|
+
calls.push({ sql: s.sql, bindings: s.params ?? [], method: 'run' });
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
_calls: calls,
|
|
109
|
+
_batchCalls: 0,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Use getter so batchCalls tracks correctly
|
|
113
|
+
Object.defineProperty(db, '_batchCalls', {
|
|
114
|
+
get: () => batchCalls,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return db;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Mock KV ─────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
function createMockKV(store: Record<string, string> = {}): KVNamespace {
|
|
123
|
+
return {
|
|
124
|
+
get: async (key: string) => store[key] ?? null,
|
|
125
|
+
put: async (key: string, value: string) => { store[key] = value; },
|
|
126
|
+
delete: async (key: string) => { delete store[key]; },
|
|
127
|
+
list: async () => ({ keys: [], list_complete: true, cacheStatus: null }),
|
|
128
|
+
getWithMetadata: async () => ({ value: null, metadata: null, cacheStatus: null }),
|
|
129
|
+
} as any;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
// ─── ensureAuthSchema / resetSchemaInit ─────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
describe('ensureAuthSchema', () => {
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
resetSchemaInit();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('calls db.batch with CREATE TABLE statements', async () => {
|
|
142
|
+
const db = createMockAuthDb();
|
|
143
|
+
await ensureAuthSchema(db);
|
|
144
|
+
expect(db._batchCalls).toBe(1);
|
|
145
|
+
// Should have prepared multiple CREATE TABLE statements
|
|
146
|
+
expect(db._calls.length).toBeGreaterThanOrEqual(5);
|
|
147
|
+
expect(db._calls.some((c) => c.sql.includes('_email_index'))).toBe(true);
|
|
148
|
+
expect(db._calls.some((c) => c.sql.includes('_oauth_index'))).toBe(true);
|
|
149
|
+
expect(db._calls.some((c) => c.sql.includes('_anon_index'))).toBe(true);
|
|
150
|
+
expect(db._calls.some((c) => c.sql.includes('_admins'))).toBe(true);
|
|
151
|
+
expect(db._calls.some((c) => c.sql.includes('_admin_sessions'))).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('idempotent — second call skips', async () => {
|
|
155
|
+
const db = createMockAuthDb();
|
|
156
|
+
await ensureAuthSchema(db);
|
|
157
|
+
const batchCount1 = db._batchCalls;
|
|
158
|
+
await ensureAuthSchema(db);
|
|
159
|
+
expect(db._batchCalls).toBe(batchCount1); // No additional batch call
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('resetSchemaInit allows re-initialization', async () => {
|
|
163
|
+
const db = createMockAuthDb();
|
|
164
|
+
await ensureAuthSchema(db);
|
|
165
|
+
const batchCount1 = db._batchCalls;
|
|
166
|
+
resetSchemaInit();
|
|
167
|
+
await ensureAuthSchema(db);
|
|
168
|
+
expect(db._batchCalls).toBe(batchCount1 + 1);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ─── Email Index ─────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
describe('lookupEmail', () => {
|
|
175
|
+
it('returns null when email not found', async () => {
|
|
176
|
+
const db = createMockAuthDb({ firstResult: null });
|
|
177
|
+
const result = await lookupEmail(db, 'missing@test.com');
|
|
178
|
+
expect(result).toBeNull();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('returns userId + shardId when confirmed', async () => {
|
|
182
|
+
const db = createMockAuthDb({
|
|
183
|
+
firstResult: { userId: 'u1', shardId: 3 },
|
|
184
|
+
});
|
|
185
|
+
const result = await lookupEmail(db, 'found@test.com');
|
|
186
|
+
expect(result).toEqual({ userId: 'u1', shardId: 3 });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('cleans stale pending before lookup', async () => {
|
|
190
|
+
const db = createMockAuthDb({ firstResult: null });
|
|
191
|
+
await lookupEmail(db, 'any@test.com');
|
|
192
|
+
// First two calls are DELETE for stale pending, third is SELECT
|
|
193
|
+
expect(db._calls.length).toBe(3);
|
|
194
|
+
expect(db._calls[0].sql).toContain('DELETE FROM _email_index');
|
|
195
|
+
expect(db._calls[0].sql).toContain('pending');
|
|
196
|
+
expect(db._calls[2].sql).toContain('SELECT userId, shardId');
|
|
197
|
+
expect(db._calls[2].bindings[0]).toBe('any@test.com');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('registerEmailPending', () => {
|
|
202
|
+
it('inserts pending email when no existing', async () => {
|
|
203
|
+
const db = createMockAuthDb({ firstResult: null });
|
|
204
|
+
await registerEmailPending(db, 'new@test.com', 'u1');
|
|
205
|
+
const insertCall = db._calls.find((c) => c.sql.includes('INSERT INTO _email_index'));
|
|
206
|
+
expect(insertCall).toBeDefined();
|
|
207
|
+
// bindings: [email, userId, 0 (hardcoded shardId), now]
|
|
208
|
+
expect(insertCall!.bindings[0]).toBe('new@test.com');
|
|
209
|
+
expect(insertCall!.bindings[1]).toBe('u1');
|
|
210
|
+
expect(insertCall!.bindings[2]).toBe(0);
|
|
211
|
+
expect(insertCall!.bindings).toHaveLength(4); // includes ISO timestamp
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('throws when email already confirmed', async () => {
|
|
215
|
+
const db = createMockAuthDb({
|
|
216
|
+
firstResult: { email: 'taken@test.com', status: 'confirmed' },
|
|
217
|
+
});
|
|
218
|
+
await expect(
|
|
219
|
+
registerEmailPending(db, 'taken@test.com', 'u2'),
|
|
220
|
+
).rejects.toThrow('EMAIL_ALREADY_REGISTERED');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('replaces pending email', async () => {
|
|
224
|
+
const db = createMockAuthDb({
|
|
225
|
+
firstResult: { email: 'pending@test.com', status: 'pending' },
|
|
226
|
+
});
|
|
227
|
+
await registerEmailPending(db, 'pending@test.com', 'u3');
|
|
228
|
+
// Should DELETE existing pending, then INSERT new
|
|
229
|
+
const deleteCall = db._calls.find(
|
|
230
|
+
(c) => c.sql.includes('DELETE FROM _email_index') && c.sql.includes('pending'),
|
|
231
|
+
);
|
|
232
|
+
expect(deleteCall).toBeDefined();
|
|
233
|
+
const insertCall = db._calls.find((c) => c.sql.includes('INSERT INTO _email_index'));
|
|
234
|
+
expect(insertCall).toBeDefined();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('confirmEmail', () => {
|
|
239
|
+
it('updates status to confirmed', async () => {
|
|
240
|
+
const db = createMockAuthDb();
|
|
241
|
+
await confirmEmail(db, 'user@test.com', 'u1');
|
|
242
|
+
expect(db._calls[0].sql).toContain('UPDATE _email_index SET status');
|
|
243
|
+
expect(db._calls[0].bindings).toEqual(['user@test.com', 'u1']);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('deleteEmailPending', () => {
|
|
248
|
+
it('deletes pending email', async () => {
|
|
249
|
+
const db = createMockAuthDb();
|
|
250
|
+
await deleteEmailPending(db, 'user@test.com');
|
|
251
|
+
expect(db._calls[0].sql).toContain('DELETE FROM _email_index');
|
|
252
|
+
expect(db._calls[0].sql).toContain('pending');
|
|
253
|
+
expect(db._calls[0].bindings[0]).toBe('user@test.com');
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('deleteEmail', () => {
|
|
258
|
+
it('deletes email regardless of status', async () => {
|
|
259
|
+
const db = createMockAuthDb();
|
|
260
|
+
await deleteEmail(db, 'user@test.com');
|
|
261
|
+
expect(db._calls[0].sql).toContain('DELETE FROM _email_index WHERE email = ?');
|
|
262
|
+
expect(db._calls[0].bindings[0]).toBe('user@test.com');
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// ─── OAuth Index ─────────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
describe('lookupOAuth', () => {
|
|
269
|
+
it('returns null when not found', async () => {
|
|
270
|
+
const db = createMockAuthDb({ firstResult: null });
|
|
271
|
+
const result = await lookupOAuth(db, 'google', 'g-user-1');
|
|
272
|
+
expect(result).toBeNull();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('returns userId + shardId when confirmed', async () => {
|
|
276
|
+
const db = createMockAuthDb({
|
|
277
|
+
firstResult: { userId: 'u1', shardId: 2 },
|
|
278
|
+
});
|
|
279
|
+
const result = await lookupOAuth(db, 'github', 'gh-123');
|
|
280
|
+
expect(result).toEqual({ userId: 'u1', shardId: 2 });
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('registerOAuthPending', () => {
|
|
285
|
+
it('inserts pending when no conflict', async () => {
|
|
286
|
+
const db = createMockAuthDb({ firstResult: null });
|
|
287
|
+
await registerOAuthPending(db, 'google', 'g-1', 'u1');
|
|
288
|
+
const insertCall = db._calls.find((c) => c.sql.includes('INSERT INTO _oauth_index'));
|
|
289
|
+
expect(insertCall).toBeDefined();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('throws when already confirmed', async () => {
|
|
293
|
+
// registerOAuthPending does 2 db.run() DELETEs then 1 db.first() SELECT
|
|
294
|
+
// The first db.first() call should return an existing confirmed record
|
|
295
|
+
const mockDb = createMockAuthDb({
|
|
296
|
+
firstResults: [{ userId: 'existing', status: 'confirmed' }],
|
|
297
|
+
});
|
|
298
|
+
await expect(
|
|
299
|
+
registerOAuthPending(mockDb, 'google', 'g-1', 'u2'),
|
|
300
|
+
).rejects.toThrow('OAUTH_ALREADY_LINKED');
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('confirmOAuth', () => {
|
|
305
|
+
it('updates status to confirmed', async () => {
|
|
306
|
+
const db = createMockAuthDb();
|
|
307
|
+
await confirmOAuth(db, 'google', 'g-1');
|
|
308
|
+
expect(db._calls[0].sql).toContain('UPDATE _oauth_index');
|
|
309
|
+
expect(db._calls[0].bindings).toEqual(['google', 'g-1']);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe('deleteOAuth', () => {
|
|
314
|
+
it('deletes OAuth record', async () => {
|
|
315
|
+
const db = createMockAuthDb();
|
|
316
|
+
await deleteOAuth(db, 'github', 'gh-1');
|
|
317
|
+
expect(db._calls[0].sql).toContain('DELETE FROM _oauth_index');
|
|
318
|
+
expect(db._calls[0].bindings).toEqual(['github', 'gh-1']);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// ─── Phone Index ─────────────────────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
describe('lookupPhone', () => {
|
|
325
|
+
it('returns null when not found', async () => {
|
|
326
|
+
const db = createMockAuthDb({ firstResult: null });
|
|
327
|
+
const result = await lookupPhone(db, '+821012345678');
|
|
328
|
+
expect(result).toBeNull();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('returns userId + shardId when confirmed', async () => {
|
|
332
|
+
const db = createMockAuthDb({
|
|
333
|
+
firstResult: { userId: 'u1', shardId: 6 },
|
|
334
|
+
});
|
|
335
|
+
const result = await lookupPhone(db, '+821012345678');
|
|
336
|
+
expect(result).toEqual({ userId: 'u1', shardId: 6 });
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe('registerPhonePending', () => {
|
|
341
|
+
it('inserts pending when no existing', async () => {
|
|
342
|
+
const db = createMockAuthDb({ firstResult: null });
|
|
343
|
+
await registerPhonePending(db, '+821000000000', 'u1');
|
|
344
|
+
const insertCall = db._calls.find((c) => c.sql.includes('INSERT INTO _phone_index'));
|
|
345
|
+
expect(insertCall).toBeDefined();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('throws when phone already confirmed', async () => {
|
|
349
|
+
const db = createMockAuthDb({
|
|
350
|
+
firstResult: { phone: '+821000000000', status: 'confirmed' },
|
|
351
|
+
});
|
|
352
|
+
await expect(
|
|
353
|
+
registerPhonePending(db, '+821000000000', 'u2'),
|
|
354
|
+
).rejects.toThrow('PHONE_ALREADY_REGISTERED');
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe('confirmPhone', () => {
|
|
359
|
+
it('updates status to confirmed', async () => {
|
|
360
|
+
const db = createMockAuthDb();
|
|
361
|
+
await confirmPhone(db, '+821000000000', 'u1');
|
|
362
|
+
expect(db._calls[0].sql).toContain('UPDATE _phone_index');
|
|
363
|
+
expect(db._calls[0].bindings).toEqual(['+821000000000', 'u1']);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe('deletePhone', () => {
|
|
368
|
+
it('deletes phone record', async () => {
|
|
369
|
+
const db = createMockAuthDb();
|
|
370
|
+
await deletePhone(db, '+821000000000');
|
|
371
|
+
expect(db._calls[0].sql).toContain('DELETE FROM _phone_index');
|
|
372
|
+
expect(db._calls[0].bindings[0]).toBe('+821000000000');
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// ─── Anonymous Index ─────────────────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
describe('registerAnonPending', () => {
|
|
379
|
+
it('inserts anon pending', async () => {
|
|
380
|
+
const db = createMockAuthDb();
|
|
381
|
+
await registerAnonPending(db, 'anon-1');
|
|
382
|
+
const insertCall = db._calls.find((c) => c.sql.includes('INSERT INTO _anon_index'));
|
|
383
|
+
expect(insertCall).toBeDefined();
|
|
384
|
+
// bindings: [userId, 0 (hardcoded shardId), now]
|
|
385
|
+
expect(insertCall!.bindings[0]).toBe('anon-1');
|
|
386
|
+
expect(insertCall!.bindings[1]).toBe(0);
|
|
387
|
+
expect(insertCall!.bindings).toHaveLength(3); // includes ISO timestamp
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('confirmAnon', () => {
|
|
392
|
+
it('updates status to confirmed', async () => {
|
|
393
|
+
const db = createMockAuthDb();
|
|
394
|
+
await confirmAnon(db, 'anon-1');
|
|
395
|
+
expect(db._calls[0].sql).toContain('UPDATE _anon_index');
|
|
396
|
+
expect(db._calls[0].bindings[0]).toBe('anon-1');
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe('deleteAnon', () => {
|
|
401
|
+
it('deletes anon record', async () => {
|
|
402
|
+
const db = createMockAuthDb();
|
|
403
|
+
await deleteAnon(db, 'anon-1');
|
|
404
|
+
expect(db._calls[0].sql).toContain('DELETE FROM _anon_index');
|
|
405
|
+
expect(db._calls[0].bindings[0]).toBe('anon-1');
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe('batchDeleteAnon', () => {
|
|
410
|
+
it('does nothing for empty array', async () => {
|
|
411
|
+
const db = createMockAuthDb();
|
|
412
|
+
await batchDeleteAnon(db, []);
|
|
413
|
+
expect(db._batchCalls).toBe(0);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('batch deletes multiple userIds', async () => {
|
|
417
|
+
const db = createMockAuthDb();
|
|
418
|
+
await batchDeleteAnon(db, ['a1', 'a2', 'a3']);
|
|
419
|
+
expect(db._batchCalls).toBe(1);
|
|
420
|
+
expect(db._calls).toHaveLength(3);
|
|
421
|
+
expect(db._calls[0].bindings[0]).toBe('a1');
|
|
422
|
+
expect(db._calls[1].bindings[0]).toBe('a2');
|
|
423
|
+
expect(db._calls[2].bindings[0]).toBe('a3');
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// ─── Admin CRUD ──────────────────────────────────────────────────────────────
|
|
428
|
+
|
|
429
|
+
describe('getAdminByEmail', () => {
|
|
430
|
+
it('returns null when not found', async () => {
|
|
431
|
+
const db = createMockAuthDb({ firstResult: null });
|
|
432
|
+
const result = await getAdminByEmail(db, 'admin@test.com');
|
|
433
|
+
expect(result).toBeNull();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('returns admin record when found', async () => {
|
|
437
|
+
const admin = { id: 'a1', email: 'admin@test.com', passwordHash: 'hash', createdAt: '2024-01-01', updatedAt: '2024-01-01' };
|
|
438
|
+
const db = createMockAuthDb({ firstResult: admin });
|
|
439
|
+
const result = await getAdminByEmail(db, 'admin@test.com');
|
|
440
|
+
expect(result).toEqual(admin);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('lazy deletes expired sessions', async () => {
|
|
444
|
+
const db = createMockAuthDb({ firstResult: null });
|
|
445
|
+
await getAdminByEmail(db, 'admin@test.com');
|
|
446
|
+
// First call should be DELETE expired sessions
|
|
447
|
+
expect(db._calls[0].sql).toContain('DELETE FROM _admin_sessions');
|
|
448
|
+
expect(db._calls[0].sql).toContain('expiresAt');
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
describe('getAdminById', () => {
|
|
453
|
+
it('returns null when not found', async () => {
|
|
454
|
+
const db = createMockAuthDb({ firstResult: null });
|
|
455
|
+
const result = await getAdminById(db, 'nonexistent');
|
|
456
|
+
expect(result).toBeNull();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('returns admin record when found', async () => {
|
|
460
|
+
const admin = { id: 'a1', email: 'admin@test.com', passwordHash: 'hash', createdAt: '2024-01-01', updatedAt: '2024-01-01' };
|
|
461
|
+
const db = createMockAuthDb({ firstResult: admin });
|
|
462
|
+
const result = await getAdminById(db, 'a1');
|
|
463
|
+
expect(result).toEqual(admin);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
describe('adminExists', () => {
|
|
468
|
+
it('returns false when no admins', async () => {
|
|
469
|
+
const db = createMockAuthDb({ firstResult: { cnt: 0 } });
|
|
470
|
+
expect(await adminExists(db)).toBe(false);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('returns true when admins exist', async () => {
|
|
474
|
+
const db = createMockAuthDb({ firstResult: { cnt: 2 } });
|
|
475
|
+
expect(await adminExists(db)).toBe(true);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('returns false when result is null', async () => {
|
|
479
|
+
const db = createMockAuthDb({ firstResult: null });
|
|
480
|
+
expect(await adminExists(db)).toBe(false);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe('createAdmin', () => {
|
|
485
|
+
it('inserts admin record', async () => {
|
|
486
|
+
const db = createMockAuthDb();
|
|
487
|
+
await createAdmin(db, 'a1', 'admin@test.com', 'hashed-pw');
|
|
488
|
+
expect(db._calls[0].sql).toContain('INSERT INTO _admins');
|
|
489
|
+
expect(db._calls[0].bindings[0]).toBe('a1');
|
|
490
|
+
expect(db._calls[0].bindings[1]).toBe('admin@test.com');
|
|
491
|
+
expect(db._calls[0].bindings[2]).toBe('hashed-pw');
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// ─── Admin Sessions ──────────────────────────────────────────────────────────
|
|
496
|
+
|
|
497
|
+
describe('getAdminSession', () => {
|
|
498
|
+
it('returns null when session not found', async () => {
|
|
499
|
+
const db = createMockAuthDb({ firstResult: null });
|
|
500
|
+
const result = await getAdminSession(db, 'bad-refresh-token');
|
|
501
|
+
expect(result).toBeNull();
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('returns session when valid', async () => {
|
|
505
|
+
const session = { id: 's1', adminId: 'a1', refreshToken: 'rt-1', expiresAt: '2099-01-01', createdAt: '2024-01-01' };
|
|
506
|
+
const db = createMockAuthDb({ firstResult: session });
|
|
507
|
+
const result = await getAdminSession(db, 'rt-1');
|
|
508
|
+
expect(result).toEqual(session);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
describe('createAdminSession', () => {
|
|
513
|
+
it('inserts session record', async () => {
|
|
514
|
+
const db = createMockAuthDb();
|
|
515
|
+
await createAdminSession(db, 's1', 'a1', 'rt-1', '2099-01-01');
|
|
516
|
+
expect(db._calls[0].sql).toContain('INSERT INTO _admin_sessions');
|
|
517
|
+
expect(db._calls[0].bindings[0]).toBe('s1');
|
|
518
|
+
expect(db._calls[0].bindings[1]).toBe('a1');
|
|
519
|
+
expect(db._calls[0].bindings[2]).toBe('rt-1');
|
|
520
|
+
expect(db._calls[0].bindings[3]).toBe('2099-01-01');
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
describe('deleteAdminSession', () => {
|
|
525
|
+
it('deletes session', async () => {
|
|
526
|
+
const db = createMockAuthDb();
|
|
527
|
+
await deleteAdminSession(db, 's1');
|
|
528
|
+
expect(db._calls[0].sql).toContain('DELETE FROM _admin_sessions');
|
|
529
|
+
expect(db._calls[0].bindings[0]).toBe('s1');
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
describe('updateAdminPassword', () => {
|
|
534
|
+
it('updates password hash and revokes all admin sessions atomically', async () => {
|
|
535
|
+
const db = createMockAuthDb();
|
|
536
|
+
await updateAdminPassword(db, 'admin-1', 'new-hash');
|
|
537
|
+
|
|
538
|
+
expect(db._batchCalls).toBe(1);
|
|
539
|
+
expect(db._calls).toHaveLength(2);
|
|
540
|
+
expect(db._calls[0].sql).toContain('UPDATE _admins SET passwordHash = ?');
|
|
541
|
+
expect(db._calls[0].bindings[0]).toBe('new-hash');
|
|
542
|
+
expect(db._calls[0].bindings[2]).toBe('admin-1');
|
|
543
|
+
expect(db._calls[1].sql).toContain('DELETE FROM _admin_sessions WHERE adminId = ?');
|
|
544
|
+
expect(db._calls[1].bindings[0]).toBe('admin-1');
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// ─── User Listing ────────────────────────────────────────────────────────────
|
|
549
|
+
|
|
550
|
+
describe('listUserMappings', () => {
|
|
551
|
+
it('returns empty when no users', async () => {
|
|
552
|
+
const db = createMockAuthDb({ firstResult: { count: 0 }, queryResult: [] });
|
|
553
|
+
const result = await listUserMappings(db, 10, 0);
|
|
554
|
+
expect(result.mappings).toEqual([]);
|
|
555
|
+
expect(result.total).toBe(0);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('returns mappings and total', async () => {
|
|
559
|
+
const db = createMockAuthDb({
|
|
560
|
+
firstResult: { count: 2 },
|
|
561
|
+
queryResult: [
|
|
562
|
+
{ userId: 'u1', shardId: 1 },
|
|
563
|
+
{ userId: 'u2', shardId: 3 },
|
|
564
|
+
],
|
|
565
|
+
});
|
|
566
|
+
const result = await listUserMappings(db, 10, 0);
|
|
567
|
+
expect(result.mappings).toHaveLength(2);
|
|
568
|
+
expect(result.total).toBe(2);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('detects hasMore when results exceed limit', async () => {
|
|
572
|
+
const db = createMockAuthDb({
|
|
573
|
+
firstResult: { count: 10 },
|
|
574
|
+
queryResult: [
|
|
575
|
+
{ userId: 'u1', shardId: 1 },
|
|
576
|
+
{ userId: 'u2', shardId: 2 },
|
|
577
|
+
{ userId: 'u3', shardId: 3 }, // extra: limit+1
|
|
578
|
+
],
|
|
579
|
+
});
|
|
580
|
+
const result = await listUserMappings(db, 2, 0);
|
|
581
|
+
expect(result.mappings).toHaveLength(2); // sliced to limit
|
|
582
|
+
expect(result.total).toBe(10);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('passes limit+1 and offset to SQL', async () => {
|
|
586
|
+
const db = createMockAuthDb({ firstResult: { count: 0 }, queryResult: [] });
|
|
587
|
+
await listUserMappings(db, 20, 40);
|
|
588
|
+
const selectCall = db._calls.find((c) => c.sql.includes('LIMIT ? OFFSET ?'));
|
|
589
|
+
expect(selectCall).toBeDefined();
|
|
590
|
+
expect(selectCall!.bindings).toEqual([21, 40]); // limit+1, offset
|
|
591
|
+
expect(selectCall!.sql).toContain('ORDER BY userId DESC');
|
|
592
|
+
expect(db._calls[0].sql).toContain('COUNT(DISTINCT userId)');
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
describe('searchUserMappingsByEmail', () => {
|
|
597
|
+
it('returns empty when no matching users', async () => {
|
|
598
|
+
const db = createMockAuthDb({ firstResult: { count: 0 }, queryResult: [] });
|
|
599
|
+
const result = await searchUserMappingsByEmail(db, 'foo@bar.com', 10, 0);
|
|
600
|
+
expect(result.mappings).toEqual([]);
|
|
601
|
+
expect(result.total).toBe(0);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('returns mappings and total', async () => {
|
|
605
|
+
const db = createMockAuthDb({
|
|
606
|
+
firstResult: { count: 2 },
|
|
607
|
+
queryResult: [
|
|
608
|
+
{ userId: 'u1', shardId: 1 },
|
|
609
|
+
{ userId: 'u2', shardId: 3 },
|
|
610
|
+
],
|
|
611
|
+
});
|
|
612
|
+
const result = await searchUserMappingsByEmail(db, 'test', 10, 0);
|
|
613
|
+
expect(result.mappings).toHaveLength(2);
|
|
614
|
+
expect(result.total).toBe(2);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it('detects hasMore when results exceed limit', async () => {
|
|
618
|
+
const db = createMockAuthDb({
|
|
619
|
+
firstResult: { count: 9 },
|
|
620
|
+
queryResult: [
|
|
621
|
+
{ userId: 'u1', shardId: 1 },
|
|
622
|
+
{ userId: 'u2', shardId: 2 },
|
|
623
|
+
{ userId: 'u3', shardId: 3 }, // extra: limit+1
|
|
624
|
+
],
|
|
625
|
+
});
|
|
626
|
+
const result = await searchUserMappingsByEmail(db, 'test', 2, 0);
|
|
627
|
+
expect(result.mappings).toHaveLength(2); // sliced to limit
|
|
628
|
+
expect(result.total).toBe(9);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('passes lowercased LIKE pattern, limit+1, and offset to SQL', async () => {
|
|
632
|
+
const db = createMockAuthDb({ firstResult: { count: 0 }, queryResult: [] });
|
|
633
|
+
await searchUserMappingsByEmail(db, 'Alice@Example.COM', 20, 40);
|
|
634
|
+
const countCall = db._calls.find((c) => c.method === 'first' && c.sql.includes('LIKE'));
|
|
635
|
+
expect(countCall).toBeDefined();
|
|
636
|
+
expect(countCall!.bindings).toEqual(['%alice@example.com%']);
|
|
637
|
+
const pagedCall = db._calls.find((c) => c.method === 'query' && c.sql.includes('LIMIT ? OFFSET ?'));
|
|
638
|
+
expect(pagedCall!.bindings).toEqual(['%alice@example.com%', 21, 40]);
|
|
639
|
+
expect(pagedCall!.sql).toContain('ORDER BY userId DESC');
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('wraps email query with % wildcards for LIKE', async () => {
|
|
643
|
+
const db = createMockAuthDb({ firstResult: { count: 0 }, queryResult: [] });
|
|
644
|
+
await searchUserMappingsByEmail(db, 'test', 10, 0);
|
|
645
|
+
const countCall = db._calls.find((c) => c.method === 'first' && c.sql.includes('LIKE'));
|
|
646
|
+
expect(countCall!.bindings[0]).toBe('%test%');
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
describe('countUsers', () => {
|
|
651
|
+
it('returns 0 when no users', async () => {
|
|
652
|
+
const db = createMockAuthDb({ firstResult: { count: 0 } });
|
|
653
|
+
expect(await countUsers(db)).toBe(0);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('returns count', async () => {
|
|
657
|
+
const db = createMockAuthDb({ firstResult: { count: 42 } });
|
|
658
|
+
expect(await countUsers(db)).toBe(42);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('returns 0 when result is null', async () => {
|
|
662
|
+
const db = createMockAuthDb({ firstResult: null });
|
|
663
|
+
expect(await countUsers(db)).toBe(0);
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// ─── KV Token Helpers ────────────────────────────────────────────────────────
|
|
668
|
+
|
|
669
|
+
describe('lookupTokenShard', () => {
|
|
670
|
+
it('returns null when token not found', async () => {
|
|
671
|
+
const kv = createMockKV();
|
|
672
|
+
const result = await lookupTokenShard(kv, 'missing-token');
|
|
673
|
+
expect(result).toBeNull();
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('returns shardId when found', async () => {
|
|
677
|
+
const kv = createMockKV({
|
|
678
|
+
'email-token:valid-token': JSON.stringify({ shardId: 7 }),
|
|
679
|
+
});
|
|
680
|
+
const result = await lookupTokenShard(kv, 'valid-token');
|
|
681
|
+
expect(result).toBe(7);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('returns null for invalid JSON', async () => {
|
|
685
|
+
const kv = createMockKV({
|
|
686
|
+
'email-token:bad-token': 'not-json',
|
|
687
|
+
});
|
|
688
|
+
const result = await lookupTokenShard(kv, 'bad-token');
|
|
689
|
+
expect(result).toBeNull();
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
describe('deleteTokenMapping', () => {
|
|
694
|
+
it('deletes token from KV', async () => {
|
|
695
|
+
const store: Record<string, string> = {
|
|
696
|
+
'email-token:tok-1': JSON.stringify({ shardId: 1 }),
|
|
697
|
+
};
|
|
698
|
+
const kv = createMockKV(store);
|
|
699
|
+
await deleteTokenMapping(kv, 'tok-1');
|
|
700
|
+
expect(store['email-token:tok-1']).toBeUndefined();
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// ─── Passkey Index ───────────────────────────────────────────────────────────
|
|
705
|
+
|
|
706
|
+
describe('lookupPasskey', () => {
|
|
707
|
+
it('returns null when not found', async () => {
|
|
708
|
+
const db = createMockAuthDb({ firstResult: null });
|
|
709
|
+
const result = await lookupPasskey(db, 'cred-missing');
|
|
710
|
+
expect(result).toBeNull();
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it('returns userId + shardId when found', async () => {
|
|
714
|
+
const db = createMockAuthDb({
|
|
715
|
+
firstResult: { userId: 'u1', shardId: 8 },
|
|
716
|
+
});
|
|
717
|
+
const result = await lookupPasskey(db, 'cred-1');
|
|
718
|
+
expect(result).toEqual({ userId: 'u1', shardId: 8 });
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
describe('registerPasskey', () => {
|
|
723
|
+
it('inserts passkey record', async () => {
|
|
724
|
+
const db = createMockAuthDb();
|
|
725
|
+
await registerPasskey(db, 'cred-1', 'u1');
|
|
726
|
+
expect(db._calls[0].sql).toContain('INSERT INTO _passkey_index');
|
|
727
|
+
expect(db._calls[0].bindings[0]).toBe('cred-1');
|
|
728
|
+
expect(db._calls[0].bindings[1]).toBe('u1');
|
|
729
|
+
expect(db._calls[0].bindings[2]).toBe(0);
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
describe('deletePasskey', () => {
|
|
734
|
+
it('deletes passkey by credentialId', async () => {
|
|
735
|
+
const db = createMockAuthDb();
|
|
736
|
+
await deletePasskey(db, 'cred-1');
|
|
737
|
+
expect(db._calls[0].sql).toContain('DELETE FROM _passkey_index WHERE credentialId');
|
|
738
|
+
expect(db._calls[0].bindings[0]).toBe('cred-1');
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
describe('deletePasskeysByUser', () => {
|
|
743
|
+
it('deletes all passkeys for a user', async () => {
|
|
744
|
+
const db = createMockAuthDb();
|
|
745
|
+
await deletePasskeysByUser(db, 'u1');
|
|
746
|
+
expect(db._calls[0].sql).toContain('DELETE FROM _passkey_index WHERE userId');
|
|
747
|
+
expect(db._calls[0].bindings[0]).toBe('u1');
|
|
748
|
+
});
|
|
749
|
+
});
|